Skip to content

test(arch): register and date every module compatibility exception - #1013

Merged
mforce merged 7 commits into
mainfrom
chore/850-compatibility-exceptions
Oct 2, 2026
Merged

mforce merged 7 commits into
mainfrom
chore/850-compatibility-exceptions

Conversation

@mforce

@mforce mforce commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Closes #850

Every read of a contracted module's tables from outside that module is now either allowed by structure or registered with an owner, a reason and a deletion trigger naming a slice issue. A guard walks the code to find them, so nobody has to remember the list.

Exceptions, before and after

Caller Before (issue #850) After
ReportQueries Platform reads db.Expenses Resolved by #856 / PR #1012. It is now an Insights type, and the guard admits it through the declared Insights -> Finance R edge whose symbols lists it
ExportQueries Platform reads db.Expenses, db.ExpenseCategories Resolved the same way
BusinessRecordModel typeof(Expense) in an 11-type Platform list Resolved by #969 / PR #970. BusinessRecordModel now lists one contribution per module, and Finance's FinanceBusinessRecords in ExpenseConfiguration.cs names typeof(Expense). It was never a DbSet read
SimulationDataSeeder handler injection CreateExpenseCategoryHandler, CreateExpenseHandler Resolved by #849 / PR #1010 (IFinanceModule)
SimulationDataSeeder.EnsureExpenseCategoryAsync undated owner Platform, deleteWhen #858. Lookup by name before CreateCategoryAsync so a re-run converges
SimulationDataSeeder.EnsureExpenseAsync undated owner Platform, deleteWhen #858. Existence check by fixture description before CreateExpenseAsync
SimulationDataSeeder.ComputeCountsAsync undated owner Platform, deleteWhen #858. Manifest counts. IFinanceModule deliberately has no count read
CurrencyBoundRowProbe.AnyAsync undated owner Farm, deleteWhen #855. Farm's currency lock also reads Commerce and General Inventory tables, so it moves once the last of those has a contract
CredentialEpochMiddleware.InvokeAsync (Farm, after #1015) not applicable before Farm's contract owner Access, deleteWhen #857. Reads Account.IsActive in the fresh per-request credential query
AccountSlugLookup.ResolveAsync, ListAccountsCliCommand.RunAsync (Farm) not applicable before Farm's contract owner Platform, deleteWhen #858. Cross-farm CLI reads
DailyEntryLockSweep.RunAsync (Farm) not applicable before Farm's contract owner Platform, deleteWhen #858. Lists every farm's id and time zone
DemoDataSeeder.MissingBaseDataAsync, SimulationDataSeeder.MissingBaseDataAsync, .SeedSecondAccountAsync, .ComputeCountsAsync (Farm) not applicable before Farm's contract owner Platform, deleteWhen #858. Account existence checks, the second-farm fixture and manifest counts

The rows live in module-ledger.json under compatibilityExceptions. They are keyed by namespace, type and member, never file:line (#632).

The guard

CompatibilityExceptionScanner compiles src/Cluckwork.Infrastructure from source with Roslyn and binds every expression. Projects that reference Infrastructure (Cluckwork.Api, Cluckwork.AppHost) compile against that compilation with errors tolerated. Any member that obtains a DbSet<T> is a read. The real EF model maps T to its tables, including owned values mapped apart and derived types, and the ledger's tables section names each table's owner. A read of a table whose owner has a ledger contract is allowed in four cases, and everything else needs a row:

  • the member's top-level type belongs to that module;
  • the type's owner has a ledger edge to the module, and the edge's symbols names the type;
  • the member's type is listed under owners.<Module>.implementations. Finance lists ExpenseCategoryRepository and ExpenseRepository;
  • the member is a DbSet property whose whole expression body is Set<T>().

Rows name the tables they cover. The guard fails on:

  • an undeclared read, printing the row to add;
  • a table the row does not name;
  • a stale row or a stale table;
  • a DbSet<T> of a type parameter;
  • a guarded name in a referencing project that does not bind;
  • an Infrastructure compile error;
  • a blank owner, reason or deleteWhen;
  • a deleteWhen that is not #<issue>;
  • an unknown owner;
  • a row naming an uncontracted module.

What it does not catch. It does not read SQL text. A helper that returns db.Expenses.AsQueryable() is attributed to the helper, not its caller. Edge-named types and listed implementations are trusted for every member. Details are in docs/decisions/850-compatibility-exceptions.md. Review rounds 1 and 2 reshaped the guard; see the round 1 and round 2 response comments.

Design choice to note for #851. The guard applies to every module that declares owners.<Module>.contract, not to Finance by name. With a Farm contract, and AccountRepository and FarmLogoRepository listed as implementations, this tree needs 8 more rows: 5 in Infrastructure and 3 in Api. Whichever of the two PRs merges second adds them. I chose this on purpose: a contract that does not cover direct table reads would leave the bypass this issue exists to close.

Mutation evidence (real tree, each reverted)

Mutation Result
Remove deleteWhen from the probe row RED: registry: compatibilityExceptions[3] has a blank or non-string 'deleteWhen'
Remove owner from the probe row RED: registry: compatibilityExceptions[3] has a blank or non-string 'owner'
Delete the probe's db.Expenses.AnyAsync line (stale row) RED: stale compatibility exception ...CurrencyBoundRowProbe.AnyAsync -> Finance ... (its trigger was #855)
Add db.Expenses.AnyAsync to PaymentRepository (new undeclared read) RED: undeclared compatibility exception ...PaymentRepository.AnyExpenseAsync -> Finance at ...PaymentRepository.cs:22
Add db.Expenses.Count() to ListAccountsCliCommand (name walk over Api) RED: undeclared compatibility exception Cluckwork.Api.Cli.ListAccountsCliCommand.RunAsync -> Finance at ...:30
No mutation GREEN

CompatibilityExceptionTests (42 fixture cases) covers each read shape (Set<T>(), an alias, a lambda, a local function, a LINQ chain), properties and nested types, every allowance and its limits, table ownership, table lists on rows, every registry error, stale rows and tables, generic helpers, fail-closed compilation, and the referencing-project walk (aliases, unbound names, look-alike names, a project that does not reference Infrastructure).

Test baselines

Measured on this machine. The first baseline was origin/main at 7ce95cf4. After merging #1015, origin/main at 4c429f08 measures 595 Application tests; this PR adds no tests to the other three projects.

Project origin/main This branch
Domain 495 495
Application 593 (595 at 4c429f08) 639 (+44 new)
AppHost 10 10
Integration 1873 1873

dotnet build Cluckwork.sln has 0 warnings and 0 errors (head 716da3ff). The coupling matrix regenerates unchanged. The ImagePin filter matches no Application tests; both ImagePin guards are in Integration and pass over the committed markdown.

Image check: the branch includes #1014, which moved Trivy out of CI, so the image check no longer fails on CVE-2026-84782 (#1006). This PR does not touch the Dockerfile.

Change map

BEFORE                                         AFTER

Finance tables (db.Expenses, db.ExpenseCategories) read by:

ReportQueries / ExportQueries (Insights)       ReportQueries / ExportQueries (Insights)
 └─► db.Expenses  ── seen only by the           └─► db.Expenses  ── allowed: Insights -> Finance
     explicit <Expense> type args                    edge names both types (guard checks it)

ExpenseCategoryRepository / ExpenseRepository  (unchanged)
 └─► db.Expense*  ── nothing checked             └─► allowed: implements a Finance port

SimulationDataSeeder (Platform)                SimulationDataSeeder (Platform)  ← code unchanged
 ├─► IFinanceModule                             ├─► IFinanceModule
 └─► db.Expenses / db.ExpenseCategories         └─► db.Expense*  ── 3 rows, owner Platform,
     ── listed only in the #850 issue body               deleteWhen #858

CurrencyBoundRowProbe (Platform)               CurrencyBoundRowProbe (Platform)  ← code unchanged
 └─► db.Expenses + 6 other tables               └─► db.Expenses  ── 1 row, owner Farm,
     ── listed only in the #850 issue body               deleteWhen #855

anything new reading db.Expenses               anything new reading db.Expenses
 └─► invisible                                  └─► CI fails, prints the row to add
module-ledger.json
  owners.Finance.contract  (from #849)
  edges[Insights -> Finance].symbols
  compatibilityExceptions[]  ← new section, 4 rows
        │
        └─► CompatibilityExceptionScanner (tests only)
              semantic walk of src/Cluckwork.Infrastructure
              + the same walk over projects referencing it (Api, AppHost)
              → undeclared read, stale row, undated row → CI fails

Files this PR shares with other open work. It adds no production code change. module-ledger.json gains only the new section. SimulationDataSeeder and CurrencyBoundRowProbe are untouched, so #851's farm-settings edits and IFarmModule.CanChangeCurrencyAsync do not collide. coupling-matrix.md is unchanged. After #851 merges, the probe row keeps its key, CurrencyBoundRowProbe.AnyAsync, as long as the method keeps its name.

A semantic walk over src/Cluckwork.Infrastructure finds every member that
obtains a DbSet<T> of an entity owned by a contracted module. The module's
own types, a type its ledger edge names, its port implementations and the
DbContext's DbSet properties are allowed; every other read needs a
compatibilityExceptions row in module-ledger.json with an owner, a reason
and a deleteWhen slice issue. Projects referencing Infrastructure are
matched by DbSet property name.

Four rows remain: three SimulationDataSeeder reads (owner Platform,
deleted by #858) and CurrencyBoundRowProbe.AnyAsync (owner Farm, deleted
by #855). ReportQueries and ExportQueries read through the declared
Insights -> Finance edge; BusinessRecordModel was split by module in #970.
@mforce

mforce commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

Review of record: Codex gpt-6.1-sol, round 1, at head f5bedac7. Posted by the coordinator. The log and probe files named below are local to the review host.

PR #1013 review

Reviewed head f5bedac73662445b930ec12a25e98f9291042b8c in /home/mforce/.cluckwork-slices/review-1013. Reviewed the eight-file diff, PR/issue context, the guard conventions, and decisions #514, #849, #846 and #850. No delegation, commits, pushes or GitHub comments.

No finding below is a product defect. This PR changes tests, registry data and documentation. The findings concern guard coverage and false failures, including behavior when #851 declares Farm's contract.

Findings

1. P2, CONFIRMED: entity aliases bypass the Api/AppHost walk

Location: tests/Cluckwork.Application.Tests/Architecture/CompatibilityExceptionScanner.cs:209.

Defect: The syntax fallback treats a Set<T>() type argument as a literal entity name, so an ordinary C# alias hides an unregistered Finance table read.

Failure scenario: Add the following to a CLI class in Api, or a class in AppHost:

using E = Cluckwork.Domain.Expenses.Expense;
public static int Count(AppDbContext db) => db.Set<E>().Count();

The lookup asks for entity E, finds nothing, and returns no read. A helper/accessor named FinancialRows that obtains its set through Set<E>() also escapes; neither its name nor its body matches the fallback. These are direct acquisitions, independent of the documented caller-attribution gap for an AsQueryable() helper.

Evidence: Review_ExternalAlias passed for both Api and AppHost with zero scanner failures; Review_ExternalRenamedProperty also passed with zero failures. An actual Cluckwork.Api.Cli.ReviewFinanceAlias containing the code above built in the solution and survived all 312 architecture tests without a row. Evidence is in 1013-review-probes.cs, 1013-review-probes.log, 1013-real-api-alias.cs, 1013-mutated-build.log and 1013-mutated-architecture.log, beside this report.

Resolve type aliases in the fallback, or bind those projects semantically. Keep a mutation that asserts an undeclared read for each project.

2. P2, CONFIRMED: entity namespace ownership contradicts table ownership

Location: tests/Cluckwork.Application.Tests/Architecture/CompatibilityExceptionScanner.cs:89.

Defect: The guard assigns the reached module from an entity's CLR namespace instead of the ledger's table owner, ignoring an existing explicit ownership override.

Failure scenario: #851 adds any nonempty Farm contract. UserRoleAssignment remains in Cluckwork.Domain.Accounts, but tables.Access owns UserRoleAssignments and tableOwnerOverrides explicitly preserves that distinction. The scanner then requires seven Infrastructure members, including Access's role-assignment repository and scope guard, to register exceptions for reaching Farm. These accesses are to an Access table, not a Farm table.

Evidence: Review_FarmCount copied the actual ledger, added only owners.Farm.contract = ["Cluckwork.Application.Features.Accounts.IAccountRepository"], and scanned the unchanged real tree. Compilation succeeded. It reported all seven UserRoleAssignmentRepository/FlockScopeGuard members as undeclared -> Farm reads. See 1013-review-probes.log, lines containing Farm contract enabled. The authoritative counterexamples are module-ledger.json:244 and module-ledger.json:391. The choice of contract type does not affect this experiment: the selector uses only Contract.Count > 0.

Use table ownership for the reached module and namespace ownership for the reader. The separate Farm-count section distinguishes these false failures from the intended work.

3. P2, CONFIRMED: implementing a Finance interface launders unrelated access

Location: tests/Cluckwork.Application.Tests/Architecture/CompatibilityExceptionScanner.cs:145.

Defect: Any interface owned by Finance exempts every member of its implementer and its nested types, even when those members implement no Finance operation and the type remains Platform-owned.

Failure scenario: A Platform utility implements IExpenseRepository, forwarding its required methods, and adds UnrelatedCategoryRead() => db.ExpenseCategories.Count(). The new operation needs no compatibility row, owner, reason or deletion trigger. An outer class implementing a Finance marker interface also exempts an unrelated nested reader. The check does not require a declared implementation type, module ownership, a contract interface, or correspondence between the queried member and an interface member.

Evidence: Added an actual Platform-owned abstract ReviewFinancePort : IExpenseRepository with all required abstract interface members and the unrelated category query. The real-tree output admitted ReviewFinancePort.UnrelatedCategoryRead as [implements a module port]. The mutated solution built with zero warnings/errors, and all 312 architecture tests passed. The fixture Review_NonFinanceImplementer additionally demonstrated a marker-interface exemption for both the outer reader and a nested reader. See 1013-real-port.cs, 1013-real-mutation.log, 1013-mutated-architecture.log and 1013-review-probes.log.

Finance's existing repository implementations need an allowance, but interface membership alone does not establish that all of a class's work belongs to Finance. Declare the trusted implementations or move them under Finance ownership; avoid automatic trust for arbitrary containing types.

4. P2, CONFIRMED: generic DbSet helpers can leave both helper and caller unregistered

Location: tests/Cluckwork.Application.Tests/Architecture/CompatibilityExceptionScanner.cs:87.

Defect: Requiring the DbSet entity argument to be an INamedTypeSymbol skips DbSet<T> with a type parameter and never substitutes the entity supplied at a call site.

Failure scenario: A new utility and its caller introduce a Finance read:

public static int Count<T>(DbContext db) where T : class => db.Set<T>().Count();
public int Run() => Helper.Count<Expense>(db);

The helper's entity argument is an ITypeParameterSymbol; the caller returns int, not DbSet<Expense>. Neither gets a row. This exceeds the documented AsQueryable() helper limitation: the documented helper is still registered, whereas this helper and its caller are both invisible.

Evidence: Review_GenericScalarHelper semantically compiled this source and asserted an empty Evaluate result. The printed read list contains only the fixture's ordinary DbSet declarations. See 1013-review-probes.cs and 1013-review-probes.log, generic scalar helper failures=0.

Bind closed generic call sites, or fail closed on unresolved DbSet entity ownership and require an explicit policy for such helpers. Add a generic helper mutation, distinct from the existing concrete Set<Expense>() fixture.

5. P2, CONFIRMED: an existing row silently covers new tables and overloads

Location: tests/Cluckwork.Application.Tests/Architecture/CompatibilityExceptionScanner.cs:109.

Defect: Deduplicating all reads by (enclosing type + member name, module) lets a narrow exception silently authorize further table accesses and even new overloads without updating its justification.

Failure scenario: SimulationDataSeeder.EnsureExpenseAsync has a row justified only by its Expense-description existence check. Add _ = await db.ExpenseCategories.CountAsync(ct); to that method. The existing row authorizes the extra table read although its reason does not describe it. Adding a distinct overload with that same member name and only the category query has the same result. This is a method/module allowance, not enforcement that every new read fails as claimed in decision #850:46 and the PR description.

Evidence: The actual seeder mutation is recorded in 1013-real-mutation.diff; the mutated solution and all 312 architecture tests passed. Independent fixture mutations Review_NewReadInsideRegisteredMember and Review_NewOverloadInsideRegisteredMember both returned zero failures with an unchanged row whose reason was expense count only. See 1013-review-probes.log and 1013-mutated-architecture.log.

Retain stable enclosing-symbol keys as #632 requires, but record and compare the accessed entity/table set, and distinguish overloads where independent permissions are intended. If blanket method/module permission is the accepted policy instead, narrow the advertised guarantee and explicitly document that additions within an existing row receive no new gate.

Mutation coverage and allowance assessment

Every survival below was observed, rather than inferred. The 21 review-probe cases passed, where passing means the observed guard result matched the probe's assertion.

Mutation Guard result Assessment
Concrete db.Set<Expense>() Undeclared read Caught
Context alias in a local variable Undeclared read Caught
DbSet alias in a local variable Undeclared read Caught
Lambda capturing db.Expenses Undeclared read Caught
LINQ query syntax over db.Expenses Undeclared read Caught
Conditional access db?.Expenses Undeclared read Caught
Concrete static helper obtaining db.Expenses Helper is undeclared Caught at acquisition
Concrete extension method obtaining db.Expenses Extension is undeclared Caught at acquisition
Base-class method obtaining db.Expenses Base method is undeclared Caught at acquisition
Api Set<E>() with an entity alias Zero failures CONFIRMED, finding 1
AppHost Set<E>() with an entity alias Zero failures CONFIRMED, finding 1
Api renamed accessor using aliased Set<E>() Zero failures CONFIRMED, finding 1
Platform implementation of an actual Finance repository port with an unrelated method Zero failures CONFIRMED, finding 3
Platform Finance-marker implementer, including a nested reader Zero failures CONFIRMED, finding 3
Generic scalar helper using Set<T>(), invoked with Expense Zero failures CONFIRMED, finding 4
Additional category read in an expense-only registered member Zero failures CONFIRMED, finding 5
Additional category-reading overload with that member name Zero failures CONFIRMED, finding 5
Unrelated read in a type already named by a ledger edge Zero failures CONFIRMED, deliberate type-wide allowance
New DbContext whose DbSet getter executes Set<Expense>().Count() before returning the set Zero failures CONFIRMED, deliberate property-wide allowance
Api DTO property totals.Expenses, with no database involved Undeclared Finance read CONFIRMED false positive, documented fallback limitation

The four allowances do not have equal evidentiary strength:

  • Own-module code: namespace ownership is an established module rule and is reasonable for the reader. It does not resolve the table-owner mismatch in finding 2.
  • Declared edge: this is an explicit, reviewable type-wide permission, not proof that a query follows the edge's stated reason. A listed Insights type can add another Finance read without any ledger edit. The fixture confirmed this. Current ReportQueries and ExportQueries remain read-only under the separate Insights guard, which also passed. Adding a new type to an edge changes the reviewed registry; broadening an already-listed type need not do so. This limitation is accepted by the stated rule, rather than a separate implementation finding.
  • Module interface: insufficient to establish ownership or delimit access; finding 3 demonstrates laundering with the actual port.
  • DbSet declaration: the implementation admits properties on any derived DbContext, not just AppDbContext, and permits arbitrary query code in the getter. The querying-getter fixture confirmed the survival. A canonical-context/simple-declaration restriction would make the exemption more defensible. This is a disclosed property-wide allowance, but its consequence should be explicit.

The fallback also generates a Farm read for the expression Cluckwork.Domain.Accounts.Roles in FlockScopeResolutionMiddleware.cs:49, because the namespace segment Accounts matches the property name. Its actual DbSet query at line 57 reads Access's UserRoleAssignments. This explains why correcting table ownership alone need not eliminate that Api false positive.

Interaction with #851: actual Farm-table counts

Counted source accesses in Infrastructure excluding bin, obj and generated migrations, then independently enabled a Farm contract in a copy of the real ledger and ran the scanner. Counts below follow the guard's acquisition definition, which includes writes, not just queries.

DbSet Access sites Distinct accessing members Queries / mutations
Accounts 17 16 15 / 2
FarmLogos 8 8 6 / 2
Total actual Farm tables 25 24 21 / 4
UserRoleAssignments, actually owned by Access 7 7 5 / 2

The two Accounts sites in SeedSecondAccountAsync collapse to one member. The Account and FarmLogo DbSet declarations are additional allowed declarations, excluded from the access-site counts. There is no separate banner table or DbSet: banner reads use FarmLogos.

Of the 24 actual Farm-table members, five need Infrastructure exception rows under the proposed policy; the other 19 already have port or edge allowances:

Additional Infrastructure row Site
DailyEntryLockSweep.RunAsync Jobs/DailyEntryLockSweep.cs:49
DemoDataSeeder.MissingBaseDataAsync Persistence/DemoDataSeeder.cs:212
SimulationDataSeeder.MissingBaseDataAsync Persistence/SimulationDataSeeder.cs:464
SimulationDataSeeder.SeedSecondAccountAsync Persistence/SimulationDataSeeder.cs:1926, also :1945
SimulationDataSeeder.ComputeCountsAsync Persistence/SimulationDataSeeder.cs:2228

Api adds three actual Farm-read rows: AccountSlugLookup.ResolveAsync, ListAccountsCliCommand.RunAsync, and CredentialEpochMiddleware.InvokeAsync. AppHost adds none on this tree.

Observed current implementation: 16 new rows, comprising 12 in Infrastructure and four in Api. Seven Infrastructure rows concern the Access-owned role-assignment table; the fourth Api row is FlockScopeResolutionMiddleware.InvokeAsync, with both the ownership issue and the namespace-name false positive described above. These eight are not actual Farm-table exceptions. Intended actual Farm-read burden: eight rows total, five Infrastructure plus three Api. The scan printed 38 Farm-classified member keys, including declarations and allowed reads.

Five legitimate Infrastructure rows, or eight including Api, is manageable and matches the PR's explicit rollout policy. Requiring immediate production refactoring is unnecessary: these can be registered with reviewed reasons and triggers. Requiring 16 rows, including Access-owned persistence falsely classified as Farm, makes the second PR unnecessarily larger and records incorrect dependencies. Do not use the uncorrected output as #851's exception inventory. These counts describe this exact head; #851's own source moves could alter them.

Evidence: Review_FarmCount in 1013-review-probes.cs; complete scanner output in 1013-review-probes.log. Direct counts used a Python source walk matching db.<property> and secondAccountDb.<property>; the actual matches were checked against the scanner's enclosing-member inventory and source files.

Ledger, docs and conventions

The four current Finance rows correctly describe the unchanged source. EnsureExpenseCategoryAsync looks up a name, EnsureExpenseAsync checks the fixture description, ComputeCountsAsync reads both Finance counts, and CurrencyBoundRowProbe.AnyAsync checks Expenses beside six Commerce/Inventory amount-carrying checks.

#858 matches the Platform composition/seeder conversion scope and the #849 amendment in issue #850. #855 matches the issue's stated final Inventory dependency for converting the whole currency probe. The probe's responsible owner: Farm is justified by Farm currency policy even though its current implementation namespace belongs to Platform; this field is responsibility, so equating it to namespace ownership would incorrectly reject the row.

The query exceptions' resolution is supported by the current Insights namespaces and Insights -> Finance edge symbols. BusinessRecordModel now consumes FinanceBusinessRecords.Contribution; Expense chronology is declared in ExpenseConfiguration.cs. It was not a DbSet acquisition. #850's historical resolution table is accurate at its pinned base. Its claims about new-read coverage need the qualifications in findings 1, 3, 4 and 5.

The registry uses enclosing symbols, not file/line keys, as #632 requires. Incomplete-field, duplicate, stale-row and semantic-compile-error tests pass. The new C# files use outside-namespace usings and file-scoped namespaces. The full solution build exercised the #985 style gate and reported zero warnings/errors.

Nits

  • P3, CONFIRMED, docs/decisions/849-module-contract.md:60: "Finance has no incoming edges today" contradicts the current Insights -> Finance edge and the updated explanation at line 76. A reader deciding when peer-module contract checks become relevant gets stale guidance. Replace that sentence with the current scope and explicitly identify the accepted Insights read edge.
  • The decision's "Raw SQL (FromSql, ...) ... is invisible" at 850-compatibility-exceptions.md:77 should distinguish SQL-text inspection from detection of a DbSet receiver. db.Accounts.FromSqlInterpolated(...) is counted when Farm is enabled. The scanner does not inspect table names inside SQL text.

Verification and cleanup

  • Original head: 26 compatibility tests passed, 1013-baseline.log.
  • Temporary review fixtures: 21 passed, 1013-review-probes.log. Their assertions document both catches and survivors.
  • Actual production-source mutations: aliased Api read, non-Finance port implementation, and extra category read inside the registered seeder member. Mutated solution build passed with zero warnings/errors; all 312 architecture tests passed, 1013-mutated-build.log and 1013-mutated-architecture.log.
  • All throwaway source edits and added test/source files were reverted or removed.
  • Restored head: solution build passed with zero warnings/errors; all 619 Application tests passed, 1013-final-build.log and 1013-final-application.log. git status --porcelain is empty and HEAD remains the assigned hash.
  • No integration/browser tests were needed for this test-only review. The known red image check under ci: move image vulnerability scanning out of CI to a weekly scan of published images #1006 was not treated as a finding.

Verdict: Request changes for the confirmed guard defects; no product defect found.

…rd gaps

Codex round 1 on #1013 found five guard defects, none in the product:

- the reached module came from the entity's namespace, so a Farm contract
  would charge Access's UserRoleAssignments reads to Farm. Reads now map
  through the EF model to tables and take the ledger's table owner;
- implementing any Finance interface exempted the whole type and its
  nested types. Trusted implementations are now listed by name under
  owners.<Module>.implementations;
- a row covered any new table its member read. Rows now name their
  tables, and an unnamed or no-longer-read table fails;
- Api/AppHost matched names, so `using E = ...Expense; Set<E>()` escaped
  and a namespace segment matched. Referencing projects now compile
  against the Infrastructure compilation; an unbound candidate fails closed;
- a DbSet<T> of a type parameter was skipped. It now fails closed.

A DbSet getter that queries is no longer a declaration.
@mforce

mforce commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

Round 1 (Codex gpt-6.1-sol at f5bedac7): response

Fixed in 46f060a2. That is one commit on top of f5bedac7, with no force-push. The review found no product defect; all five findings were guard defects, and all five are fixed. Each reviewer mutation below was applied to the real tree, went red with the fix, and was then reverted.

# Finding Change in 46f060a2 Evidence
1 Entity namespace decides the module reached, so Access's UserRoleAssignments would count as Farm A read now maps T to its tables through the real EF model (TableOwnerScanner.TableStoreObjects) and takes the ledger's tables owner. The reader is still classified by namespace. Api/AppHost now compile semantically, so Cluckwork.Domain.Accounts.Roles binds to a namespace and is no longer a candidate Farm experiment, rerun on this head. I copied the ledger and added owners.Farm.contract. I also listed the two Farm port types the old rule trusted, AccountRepository and FarmLogoRepository, as implementations. The scan now needs 8 rows: 5 Infrastructure (DailyEntryLockSweep.RunAsync, DemoDataSeeder.MissingBaseDataAsync, SimulationDataSeeder.MissingBaseDataAsync, .SeedSecondAccountAsync, .ComputeCountsAsync) and 3 Api (AccountSlugLookup.ResolveAsync, ListAccountsCliCommand.RunAsync, CredentialEpochMiddleware.InvokeAsync). There are 0 UserRoleAssignments reads classified as Farm, 0 unresolved and 0 registry errors. Fixture TheLedgersTableOwner_DecidesTheModuleReached
2 Implementing any Finance interface launders the whole type and its nested types Automatic trust is removed. owners.Finance.implementations lists ExpenseCategoryRepository and ExpenseRepository. Each entry must be declared in Infrastructure, implement one of the module's interfaces and read its tables; otherwise it is a registry error. Nested types are not covered Real tree: the reviewer's ReviewFinancePort : IExpenseRepository goes RED with undeclared compatibility exception ...ReviewFinancePort.UnrelatedCategoryRead -> Finance. Fixtures: ImplementingAModuleInterface_IsTrustedOnlyWhenTheLedgerListsTheType, ListedImplementation_DoesNotCoverItsNestedTypes, ImplementationThatIsNotAPort_FailsTheRegistry (2 cases), ImplementationThatReadsNothing_FailsTheRegistry
3 A row silently covers new tables and overloads Each row now names its tables. Keys stay type.member (#632). A registered member reading an unnamed table fails. A named table no longer read fails as stale. A table the ledger does not give to reaches is a registry error. Overloads share one key, and therefore one table list; the decision record says so Real tree: the reviewer's extra db.ExpenseCategories.CountAsync in EnsureExpenseAsync goes RED with ...EnsureExpenseAsync -> Finance reads table 'ExpenseCategories' ... which its row does not name. Fixtures: RegisteredMemberReadingATableItsRowDoesNotName_Fails, RowNamingATableTheMemberNoLongerReads_IsStale, plus tables cases in IncompleteRow_FailsTheRegistry
4 An entity alias escapes the Api/AppHost name walk Projects referencing Infrastructure now compile against the Infrastructure compilation, with errors tolerated, and are walked semantically like Infrastructure. A guarded property name or a Set<...> that does not bind fails closed as unresolved Real tree: the reviewer's ReviewFinanceAlias goes RED in Api (...Api.Cli.ReviewFinanceAlias.Count -> Finance) and in AppHost (...AppHost.ReviewFinanceAlias.Count -> Finance). Fixtures: ReadInAProjectReferencingTheSemanticProject_IsBoundSemantically (alias, direct read, renamed accessor), UnboundCandidateInAReferencingProject_FailsClosed, NamesThatOnlyLookLikeAGuardedSet_AreNotReads (DTO totals.Expenses, a namespace segment, HashSet)
5 A generic Set<T>() helper leaves helper and caller unregistered A DbSet<T> whose T is a type parameter fails closed as unresolved, with no allowance. The real tree has none Real tree: a ReviewGenericHelper.Count<T>(DbContext) called with Expense goes RED with ...ReviewGenericHelper.Count ... obtains DbSet<T> of a type parameter. Fixture GenericDbSetHelper_FailsClosed

Allowances the review called deliberate.

  • The DbSet-declaration allowance is narrowed. It now admits only a property whose expression body is the set itself, Expenses => Set<Expense>(). The reviewer's querying getter is now a read; fixture DbSetGetterThatQueries_IsNotADeclaration.
  • Edge-named types and listed implementations stay trusted for every member. The decision record states this as a limit.
  • Generic Identity entities now map to their tables. The new model lookup also had to match generic Identity entities such as IdentityUserRole<Guid>, so both sides are keyed by the full generic name.

Nits.

  • 849-module-contract.md no longer says Finance has no incoming edges. It names Insights -> Finance as the accepted read.
  • 850-compatibility-exceptions.md now says the scanner sees a DbSet receiver used with FromSql*, but does not read table names in SQL text.

Tests on 46f060a2. dotnet build Cluckwork.sln has 0 warnings and 0 errors. Domain 495, Application 634 (+15 since f5bedac7: 39 fixture cases and 2 real-tree tests in total), AppHost 10, Integration 1873, all passing. The coupling matrix regenerates unchanged. The red image check is still expected under #1006.

@mforce mforce closed this Oct 2, 2026
@mforce mforce reopened this Oct 2, 2026
@mforce

mforce commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

Review of record: Codex gpt-6.1-sol, round 2, at head 46f060a2. Posted by the coordinator. The log and probe files named below are local to the review host.

PR #1013, round 2 review

Reviewed 46f060a2b81b0cec0afcde03492a9b7726e31069, the nine-file fix-only diff, and the worker's response at #1013 (comment). All work stayed in the assigned detached checkout and the review-output directory. No delegation, commits, pushes or GitHub comments.

The five round-one mutations are addressed, and the Farm experiment now produces eight genuine rows. Three further guard defects remain. None is a product defect: this PR changes architecture tests, registry data and documentation. The production-source changes described below were temporary review mutations and have been reverted.

Findings

1. P2, CONFIRMED: generic arity is erased from trusted implementation keys

Location: tests/Cluckwork.Application.Tests/Architecture/CompatibilityExceptionScanner.cs:65, used by Key at line 405, the allowance at line 242, and implementation validation at line 306.

Defect: The implementation key omits generic type parameters, so a listed non-generic type automatically trusts a different, unlisted generic type with the same namespace and name.

Failure scenario: Finance explicitly lists the existing non-generic ExpenseRepository. Add this separate type in the same namespace:

public static class ExpenseRepository<T>
{
    public static int UnrelatedCategoryRead(AppDbContext db)
        => db.ExpenseCategories.Count();
}

C# distinguishes these types by arity. The guard collapses both to Cluckwork.Infrastructure.Repositories.ExpenseRepository. The generic type implements no Finance interface and is absent from the registry, yet its read gets [declared implementation]. Validation also groups declarations by that lossy key and validates the existing non-generic representative instead of rejecting the new type.

Evidence: Review_GenericImplementationKeyCollision returned zero failures. The actual source mutation above built with zero warnings/errors and survived all 327 existing architecture tests. The real-tree output explicitly reports ExpenseRepository.UnrelatedCategoryRead -> Finance [declared implementation] at ReviewGenericCollision.cs:8. Evidence files beside this report: 1013-r2-probes.cs, 1013-r2-probes.log, 1013-r2-real-generic.cs, 1013-r2-mutated-build.log, and 1013-r2-mutated-architecture.log.

Preserve arity/generic parameters in type keys and validate each exact declaration. The same key format is used for enclosing-member and edge matching, so apply the correction consistently. This is distinct from the deliberately shared key for method overloads.

2. P2, CONFIRMED: an expression-bodied getter can still query under the declaration exemption

Location: tests/Cluckwork.Application.Tests/Architecture/CompatibilityExceptionScanner.cs:247, compounded by first-site deduplication at line 192.

Defect: Testing only whether a DbSet-valued expression has an arrow-clause parent admits arbitrary expression-bodied computations, and the selected declaration site hides other query sites in the same getter.

Failure scenario: Replace the existing context property with:

public DbSet<Expense> Expenses
    => (Set<Expense>().Count(), Set<Expense>()).Item2;

Accessing the property executes a count query before returning the set. The outer .Item2 expression has type DbSet<Expense> and sits directly under the arrow clause, so it gets the declaration exemption. The inner Set<Expense>() expressions are genuine registered reads, but they share the same member/module/table key. The outer expression is visited first and wins the same-line tie, discarding the stricter classification.

Evidence: Review_QueryInsideExpressionBodiedGetter compiled and returned zero failures. Applied the expression to the actual AppDbContext.Expenses; the solution built cleanly and all 327 existing architecture tests passed. The output still classifies AppDbContext.Expenses as [DbSet declaration]. See 1013-r2-getter-mutation.diff, 1013-r2-probes.log, 1013-r2-mutated-build.log, and 1013-r2-mutated-architecture.log.

Validate the complete getter expression as a simple set declaration, rather than accepting any DbSet-valued arrow expression. Also preserve a registered classification when any acquisition within a member/table group requires it. This contradicts the corrected rule at 850-compatibility-exceptions.md:44, which says the body must be the set itself and a querying getter is a read.

3. P2, CONFIRMED: a separately mapped owned table is absent from the principal's table inventory

Location: tests/Cluckwork.Application.Tests/Architecture/CompatibilityExceptionScanner.cs:80.

Defect: The lookup excludes owned entities and assigns a DbSet only its principal entity's direct table mappings, so materializing an automatically loaded owned value can read a contracted table without the guard seeing that table.

Failure scenario: Map the existing SalesOrder.TotalAmount owned value to a separate FinanceOwnedTotals table, assign that table to Finance in a copy of the ledger with an explicit ownership-override reason, and add a Platform reader using db.SalesOrders.ToListAsync(). EF loads the owned value automatically. The generated query joins the Finance table, but the guard sees only the principal's Commerce table. Because Commerce has no contract on this head, the Platform reader produces no compatibility exception at all for the Finance data it reads.

Evidence: Temporarily added m.ToTable("FinanceOwnedTotals") to the real SalesOrderConfiguration and added ReviewOwnedRead.ReadAsync. Review_RealOwnedTableReadIsInvisible used the actual AppDbContext model and asserted both of these observations:

FROM "SalesOrders" AS s
LEFT JOIN "FinanceOwnedTotals" AS f ON s."Id" = f."SalesOrderId"
  • ToQueryString() contains the new table and selects its amount/currency columns.
  • Scanning the real source with the amended ledger yields zero failures and no read naming FinanceOwnedTotals.

The test passed; no database was contacted. Evidence: 1013-r2-owned-mutation.diff, 1013-r2-real-owned-reader.cs, 1013-r2-mapping-probes.cs, and 1013-r2-mapping.log.

Expand the principal's reachable table inventory for owned values, or explicitly fail closed on model shapes whose queried table set cannot be classified. Direct CLR/table mapping alone is insufficient for the claim that these exception rows enumerate the tables a reader accesses. This is an EF mapping gap, not the documented SQL-text or returned-helper limitation.

A related PLAUSIBLE extension concerns polymorphic base sets. An independent real EF model using TPT produced a Set<Base>() query joining DerivedFinance, while TableStoreObjects(Base) returned only Bases. This confirms the metadata omission, but I did not introduce an inheritance hierarchy into Cluckwork's production model and run the complete scanner on it. The SQL and mapping output are in 1013-r2-mapping.log.

Round-one mutations rerun

The original five findings now fail for the correct guard reasons. Rechecks used the same mutation shapes, not just the worker's supplied test assertions.

Original mutation Round-two result Evidence
Api Set<E>() using an Expense alias Undeclared Finance read 1013-r2-real-api-alias.log
AppHost Set<E>() using an Expense alias Undeclared Finance read 1013-r2-real-apphost-alias.log
Renamed Api accessor obtaining its set through Set<E>() Accessor and consuming member are both undeclared Review_ExternalRenamedProperty, 1013-r2-probes.log
Actual Platform IExpenseRepository implementer with an unrelated category read Undeclared Finance read 1013-r2-real-port.log
Finance-marker implementer and unrelated nested reader, neither listed Both reads undeclared Review_NonFinanceImplementer, 1013-r2-probes.log
Extra category access inside registered EnsureExpenseAsync ExpenseCategories absent from the row 1013-r2-real-new-table.log
New category-reading overload of registered EnsureExpenseAsync ExpenseCategories absent from the shared row 1013-r2-real-new-overload.log
Generic scalar helper using Set<T>(), called with Expense Unresolved type-parameter DbSet 1013-r2-real-generic-helper.log
Original block getter that queries before returning the set Undeclared getter read 1013-r2-real-querying-getter.log
Farm contract incorrectly classifying UserRoleAssignments as Farm No such reads or rows remain Farm experiment below
Api DTO property totals.Expenses with no database involved Zero failures Review_ExternalFalsePositive, 1013-r2-probes.log

The seven actual-source RED rechecks ran dotnet test tests/Cluckwork.Application.Tests --no-build --filter FullyQualifiedName~CompatibilityExceptionRealTreeTests --logger 'console;verbosity=detailed'. Each log contains the expected guard diagnostic; none relied on a normal build failure. The script 1013-r2-real-rechecks.py records and restores every mutation. The AppHost injected-source check establishes scanner behavior; it does not claim that adding a DbContext reader to AppHost's current resource-only project references is itself a buildable feature.

The direct Set<Expense>(), context and set aliases, lambda, LINQ query syntax, conditional access, concrete static helper, extension method and base-class cases still report the enclosing acquisition as undeclared. All 26 review-probe cases passed, with their assertions checking the expected catch or survival. The original compatibility suite's 41 cases also passed.

Harder checks and remaining scope

Implementation entry outside Finance's namespace. This is intentional and necessary for the two existing Platform-namespaced repository implementations. An explicitly listed outside-module port implementation passed; its unlisted nested type remained undeclared. An outside-module type receives no trust merely by implementing an interface. The registry's existing missing-type, non-port and no-read cases pass. The hole is the exact-type key collision in finding 1, not the deliberate namespace choice.

Owned mappings, splitting, inheritance and joins. The real owned-table mutation escaped as finding 3. The separate mapping experiment also showed that an entity's SplitToTable fragment is returned, so ordinary entity splitting is represented. Existing owned values sharing their principal's table do not add another table name, and the current suite stays green. TPT base-query mappings omit the derived table as described above. Shared CLR join types are explicitly excluded from the lookup; a direct Set<Dictionary<string, object>>(name) cannot be classified by this lookup and fails closed rather than silently receiving an allowance. The separate EF experiment verified the shared-type model shape, not a many-to-many query through Cluckwork's model. Navigation-driven join-table reads require the same query-table versus direct-mapping distinction exposed by finding 3.

Compiler errors tolerated. A missing external type in an unrelated Api field did not erase the member's bound db.Expenses read. Conversely, an Api member taking MissingContext and returning db.FinancialRows produced zero failures because neither FinancialRows nor its receiver bound and the name is outside the fallback's candidate set. This is a CONFIRMED scanner blind spot, disclosed at 850-compatibility-exceptions.md:98. A valid production read becoming unbound because its package metadata is absent remains a PLAUSIBLE coverage risk, not an additional confirmed P2 here. The fixture itself deliberately lacks the context definition.

I also attempted a real AppHost consumer of an Api helper returning a DbSet. That experiment failed the ordinary build because AppHost's existing Aspire resource references do not expose the required runtime types. A further temporary reference edit still did not yield a valid build. I discarded and reverted it; it is not evidence of a product-valid survivor. Saved exploratory files/logs 1013-r2-api-factory.cs, 1013-r2-apphost-consumer.cs and 1013-r2-lenient-build.log are excluded from the confirmed findings.

False positives. The former DTO-property and namespace-segment collisions are gone. The original head and full Application suite pass. The generic-helper refusal and unknown-model-entity refusal are intentionally conservative. I found no new false positive on the current production tree.

Farm-count experiment

Independently copied this head's ledger and added:

  • owners.Farm.contract = ["Cluckwork.Application.Features.Accounts.IAccountRepository"];
  • owners.Farm.implementations = ["Cluckwork.Infrastructure.Repositories.AccountRepository", "Cluckwork.Infrastructure.Repositories.FarmLogoRepository"].

These are experiment inputs rather than changes to the committed ledger. Any nonempty valid Farm contract exercises the same selector. The explicit implementation list is now necessary; omitting it would intentionally register the repository accesses rather than reproduce the worker's proposed rollout.

Confirmed: eight genuine additional rows, all reading Accounts. The scan has zero unresolved reads and zero registry errors. No UserRoleAssignments read is classified as Farm, and no Farm row arises from the namespace expression in FlockScopeResolutionMiddleware. There are 29 Farm-classified member/table entries including declarations and allowed reads.

Module/project Full member symbol Source
Infrastructure Cluckwork.Infrastructure.Jobs.DailyEntryLockSweep.RunAsync src/Cluckwork.Infrastructure/Jobs/DailyEntryLockSweep.cs:49
Infrastructure Cluckwork.Infrastructure.Persistence.DemoDataSeeder.MissingBaseDataAsync src/Cluckwork.Infrastructure/Persistence/DemoDataSeeder.cs:212
Infrastructure Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.MissingBaseDataAsync src/Cluckwork.Infrastructure/Persistence/SimulationDataSeeder.cs:464
Infrastructure Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.SeedSecondAccountAsync src/Cluckwork.Infrastructure/Persistence/SimulationDataSeeder.cs:1926, also :1945
Infrastructure Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.ComputeCountsAsync src/Cluckwork.Infrastructure/Persistence/SimulationDataSeeder.cs:2228
Api Cluckwork.Api.Cli.AccountSlugLookup.ResolveAsync src/Cluckwork.Api/Cli/SuspendAccountCliCommand.cs:99
Api Cluckwork.Api.Cli.ListAccountsCliCommand.RunAsync src/Cluckwork.Api/Cli/ListAccountsCliCommand.cs:31
Api Cluckwork.Api.Middleware.CredentialEpochMiddleware.InvokeAsync src/Cluckwork.Api/Middleware/CredentialEpochMiddleware.cs:54

AppHost adds none. Logo and banner data remain in FarmLogos, whose repository is explicitly trusted. The original direct-access count remains 17 Accounts sites and eight FarmLogos sites, with two Accounts accesses sharing the seeder member key. Eight reviewed rows are a manageable intended cost for #851.

Evidence: Review_FarmCount in 1013-r2-probes.cs, with the complete read inventory and eight generated rows in 1013-r2-probes.log.

Judgment on the three kept limits

  1. Type-wide trust for edge-named and explicitly listed implementation types: acceptable as documented. It is a reviewed permission for one exact type and target module, not evidence that each new method matches the existing reason. The explicit list fixes automatic interface laundering. Nested implementation types are excluded. This acceptance requires exact type identity, so it does not excuse finding 1. Current Insights read-only guards provide additional checks for the edge-named query types.
  2. Overloads share a row: acceptable as documented. They share the same table set, owner, reason and deletion trigger. A new table in another overload now fails. An additional overload using only already-approved tables remains allowed. This is a finite shared permission and consistent with the established enclosing-member convention; it should not be advertised as detecting every new query.
  3. SQL text is not read: acceptable as documented for this DbSet guard. DbSet receivers used by FromSql* are still recognized; additional table names inside SQL and Database.SqlQuery/ExecuteSql remain outside this mechanism. The tenant-bypass guard classifies SQL sites for its own invariant; it is not a substitute for module-table ownership review. The corrected decision record states the limit accurately.

The lenient compiler's unbound-renamed-expression gap is a separate disclosed limitation. It prevents treating the referencing-project walk as fully fail closed. It is not silently promoted to a confirmed product-valid defect by the invalid AppHost experiment.

Ledger, documentation and nits

The existing Finance rows still have appropriate owners, reasons and deletion triggers: seeder conversion at #858 and the full currency probe's final Inventory dependency at #855. Their new table lists match the actual direct reads. Stable enclosing-symbol keys preserve #632. The added implementation lists are appropriate for the existing repository types; they need the exact-type correction in finding 1.

Both round-one documentation nits are fixed: #849 names the accepted incoming Insights read edge, and #850 distinguishes DbSet receivers from SQL-text inspection. No additional cosmetic nits. The expression-bodied getter claim requires correction with finding 2. If owned/polymorphic or unbound-renamed reads remain deliberately outside coverage, the broad coverage claims in AGENTS.md and the decision should say so explicitly.

Verification and cleanup

  • Clean-head compatibility suite: 41 passed, 1013-r2-baseline.log.
  • Independent review probes: 26 passed, 1013-r2-probes.log.
  • EF mapping probes: 2 passed, 1013-r2-mapping.log. They generated SQL without contacting a database.
  • Seven actual-source round-one rechecks: each RED for its expected guard reason, 1013-r2-real-*.log.
  • Actual new getter and generic-type mutations: solution build passed with 0 warnings/errors; all 327 existing architecture tests passed, 1013-r2-mutated-build.log and 1013-r2-mutated-architecture.log.
  • All temporary production, project, registry and test changes reverted or removed. git status --porcelain is empty; HEAD is still 46f060a2b81b0cec0afcde03492a9b7726e31069.
  • Restored head: full solution build passed with 0 warnings/errors, including build: enforce file-scoped namespaces and using placement at build time #985's style gate; all 634 Application tests passed, 1013-r2-final-build.log and 1013-r2-final-application.log.
  • No integration or browser run was required for this guard review. The known image failure under ci: move image vulnerability scanning out of CI to a weekly scan of published images #1006 remains out of scope.

Verdict: Request changes for three confirmed new guard defects; the round-one mutations and eight-row Farm rollout are verified, and no product defect was found.

mforce added 2 commits October 1, 2026 22:31
… derived tables

Codex round 2 on #1013 found three guard gaps, none in the product:

- type keys dropped generic parameters, so ExpenseRepository<T> inherited
  the listed ExpenseRepository's trust. Keys now keep type parameters;
- an expression-bodied getter that queried before returning the set
  passed as a declaration. Only a whole body of `Set<T>()` passes, and
  the stricter classification wins within a member and table;
- a read saw only the entity's own tables. It now also includes owned
  values mapped to other tables and derived types' tables.
@mforce

mforce commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner Author

Round 2 (Codex sol at 46f060a2): response

Fixed in 3e8ff29e. That is one commit on top of 46f060a2, with no force-push. While I was fixing, the branch was updated from main with #1014 (ade75309). I merged that update in rather than force-pushing, so the PR head is the merge commit 18c7a2db. The review found no product defect. Each reviewer mutation below was applied to the real tree, went red with the fix, and was then reverted.

# Finding Change in 3e8ff29e Evidence
1 Generic arity erased from type keys TypeFormat now keeps type parameters. One key format serves members, edges, implementation trust and the validation lookup, so ExpenseRepository<T> and ExpenseRepository are different keys Real tree: the reviewer's ReviewGenericCollision.cs goes RED with undeclared compatibility exception Cluckwork.Infrastructure.Repositories.ExpenseRepository<T>.UnrelatedCategoryRead -> Finance. Fixture: ListedImplementation_DoesNotCoverAGenericTypeOfTheSameName
2 Expression-bodied getter passes as a declaration The declaration allowance now requires the property's whole expression body to be an argument-free Set<T>(). When one member reads a table more than one way, the stricter classification wins Real tree: the reviewer's (Set<Expense>().Count(), Set<Expense>()).Item2 on AppDbContext.Expenses goes RED with undeclared compatibility exception Cluckwork.Infrastructure.Persistence.AppDbContext.Expenses -> Finance. Fixture: DbSetGetterThatQueries_IsNotADeclaration now covers both the tuple getter and the block getter
3 Owned or derived tables mapped apart are invisible A set's tables are now its own, plus the tables of its owned values (followed recursively), plus the tables of its derived types, all read from the EF model. There is no general EF analysis Real tree: with the reviewer's m.ToTable("FinanceOwnedTotals") mutation, the ReviewOwnedRead reader, and FinanceOwnedTotals given to Finance in the ledger, the test goes RED. The existing SalesOrders readers now report the Finance table, for example undeclared compatibility exception ...SimulationDataSeeder.FindOrderAsync -> Finance and ...DemoDataSeeder.CleanupPartialSeedAsync -> Finance. Fixture: QueriedTables_IncludeOwnedAndDerivedTablesMappedApart builds a small model with one owned value on its own table, one on the principal's table, and a TPT derived type, and expects exactly OrderTotals, Orders and SpecialShared. The clean tree's tables are unchanged, because no owned value in Cluckwork is mapped to its own table today

Docs. The decision record and AGENTS.md now describe the narrower declaration rule, generic type keys, and owned and derived tables. They also state that tables reached through navigations (Include, LINQ joins) are not added to a read's tables.

Tests on 18c7a2db. dotnet build Cluckwork.sln has 0 warnings and 0 errors. Domain 495, Application 637 (+3 since 46f060a2), AppHost 10, Integration 1873, all passing. With #1014 merged in, Trivy no longer runs in CI.

The review loop was stopped deliberately by the owner after two consecutive rounds that found no product defect (rounds 1 and 2 found only guard gaps). No round 3 will be triggered.

mforce added 3 commits October 2, 2026 05:42
#851 gave Farm a contract, so the #850 guard now covers Farm's tables.
Farm lists AccountRepository and FarmLogoRepository as implementations,
and eight Accounts readers are registered: CredentialEpochMiddleware
(owner Access, deleted by #857), and the daily lock sweep, both seeders'
MissingBaseDataAsync, SeedSecondAccountAsync, ComputeCountsAsync,
AccountSlugLookup and list-accounts (owner Platform, deleted by #858).
@mforce

mforce commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

Farm follow-up after #1015

716da3ff registers the Farm exceptions. It sits on 0c3d2024, which merges origin/main at 4c429f08 (#1015) into this branch. This is not a new review round.

Farm now declares a contract, so the guard covers Farm's tables. Farm lists AccountRepository and FarmLogoRepository as implementations. The guard then reported exactly the 8 readers the reviewer predicted, all reading the Accounts table:

Member Owner Deleted by
Cluckwork.Api.Middleware.CredentialEpochMiddleware.InvokeAsync Access #857 (scope 4, epoch-verifier port; the read must stay a fresh read on every request)
Cluckwork.Api.Cli.AccountSlugLookup.ResolveAsync Platform #858 (scope 4, CLI verbs call contracts; seed, suspend, reactivate and rename all use it)
Cluckwork.Api.Cli.ListAccountsCliCommand.RunAsync Platform #858 (scope 4)
Cluckwork.Infrastructure.Jobs.DailyEntryLockSweep.RunAsync Platform #858 (scope 2, jobs call contracts)
Cluckwork.Infrastructure.Persistence.DemoDataSeeder.MissingBaseDataAsync Platform #858 (scope 3, seeders)
Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.MissingBaseDataAsync Platform #858 (scope 3)
Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.SeedSecondAccountAsync Platform #858 (scope 3)
Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.ComputeCountsAsync Platform #858 (scope 3)

I put AccountSlugLookup under #858 rather than #857 because it is a CLI helper that resolves a farm code before any tenant is set. Its callers are the seed verb and the account lifecycle verbs, and #858 owns CLI conversion. As #851 decided, no read moves behind IFarmModule. ReportQueries.AccountCurrencyAsync needs no row, because the declared Insights -> Farm edge names ReportQueries.

Tests on 716da3ff. dotnet build Cluckwork.sln has 0 warnings and 0 errors. Domain 495, Application 639 (main measures 595), AppHost 10, Integration 1873, all passing. The coupling matrix regenerates unchanged.

@mforce
mforce merged commit 6e3f584 into main Oct 2, 2026
16 checks passed
@mforce
mforce deleted the chore/850-compatibility-exceptions branch October 2, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C] #514 slice 8: close or date every compatibility exception

1 participant