Repository navigation
test(arch): register and date every module compatibility exception - #1013
Conversation
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.
|
Review of record: Codex PR #1013 reviewReviewed head 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. Findings1. P2, CONFIRMED: entity aliases bypass the Api/AppHost walkLocation: Defect: The syntax fallback treats a 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 Evidence: 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 ownershipLocation: 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. Evidence: 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 accessLocation: 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 Evidence: Added an actual Platform-owned abstract 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 unregisteredLocation: Defect: Requiring the DbSet entity argument to be an 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 Evidence: 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 5. P2, CONFIRMED: an existing row silently covers new tables and overloadsLocation: Defect: Deduplicating all reads by Failure scenario: Evidence: The actual seeder mutation is recorded in 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 assessmentEvery 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.
The four allowances do not have equal evidentiary strength:
The fallback also generates a Farm read for the expression Interaction with #851: actual Farm-table countsCounted source accesses in Infrastructure excluding
The two 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:
Api adds three actual Farm-read rows: 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 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: Ledger, docs and conventionsThe four current Finance rows correctly describe the unchanged source.
The query exceptions' resolution is supported by the current Insights namespaces and 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
Verification and cleanup
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.
Round 1 (Codex
|
| # | 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; fixtureDbSetGetterThatQueries_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.mdno longer says Finance has no incoming edges. It namesInsights -> Financeas the accepted read.850-compatibility-exceptions.mdnow says the scanner sees aDbSetreceiver used withFromSql*, 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.
|
Review of record: Codex PR #1013, round 2 reviewReviewed 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. Findings1. P2, CONFIRMED: generic arity is erased from trusted implementation keysLocation: 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 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 Evidence: 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 exemptionLocation: 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 Evidence: 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 3. P2, CONFIRMED: a separately mapped owned table is absent from the principal's table inventoryLocation: 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 Evidence: Temporarily added FROM "SalesOrders" AS s
LEFT JOIN "FinanceOwnedTotals" AS f ON s."Id" = f."SalesOrderId"
The test passed; no database was contacted. Evidence: 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 Round-one mutations rerunThe original five findings now fail for the correct guard reasons. Rechecks used the same mutation shapes, not just the worker's supplied test assertions.
The seven actual-source RED rechecks ran The direct Harder checks and remaining scopeImplementation 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 Compiler errors tolerated. A missing external type in an unrelated Api field did not erase the member's bound 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 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 experimentIndependently copied this head's ledger and added:
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
AppHost adds none. Logo and banner data remain in Evidence: Judgment on the three kept limits
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 nitsThe 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
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. |
… 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.
Round 2 (Codex
|
| # | 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.
…ns' into chore/850-compatibility-exceptions
…ty-exceptions # Conflicts: # AGENTS.md
#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).
Farm follow-up after #1015
Farm now declares a contract, so the guard covers Farm's tables. Farm lists
I put Tests on |
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
ReportQueriesdb.ExpensesInsights -> FinanceRedge whosesymbolslists itExportQueriesdb.Expenses,db.ExpenseCategoriesBusinessRecordModeltypeof(Expense)in an 11-type Platform listBusinessRecordModelnow lists one contribution per module, and Finance'sFinanceBusinessRecordsinExpenseConfiguration.csnamestypeof(Expense). It was never a DbSet readSimulationDataSeederhandler injectionCreateExpenseCategoryHandler,CreateExpenseHandlerIFinanceModule)SimulationDataSeeder.EnsureExpenseCategoryAsyncCreateCategoryAsyncso a re-run convergesSimulationDataSeeder.EnsureExpenseAsyncCreateExpenseAsyncSimulationDataSeeder.ComputeCountsAsyncIFinanceModuledeliberately has no count readCurrencyBoundRowProbe.AnyAsyncCredentialEpochMiddleware.InvokeAsync(Farm, after #1015)Account.IsActivein the fresh per-request credential queryAccountSlugLookup.ResolveAsync,ListAccountsCliCommand.RunAsync(Farm)DailyEntryLockSweep.RunAsync(Farm)DemoDataSeeder.MissingBaseDataAsync,SimulationDataSeeder.MissingBaseDataAsync,.SeedSecondAccountAsync,.ComputeCountsAsync(Farm)The rows live in
module-ledger.jsonundercompatibilityExceptions. They are keyed by namespace, type and member, neverfile:line(#632).The guard
CompatibilityExceptionScannercompilessrc/Cluckwork.Infrastructurefrom 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 aDbSet<T>is a read. The real EF model mapsTto its tables, including owned values mapped apart and derived types, and the ledger'stablessection names each table's owner. A read of a table whose owner has a ledgercontractis allowed in four cases, and everything else needs a row:symbolsnames the type;owners.<Module>.implementations. Finance listsExpenseCategoryRepositoryandExpenseRepository;DbSetproperty whose whole expression body isSet<T>().Rows name the
tablesthey cover. The guard fails on:DbSet<T>of a type parameter;owner,reasonordeleteWhen;deleteWhenthat is not#<issue>;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 indocs/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, andAccountRepositoryandFarmLogoRepositorylisted 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)
deleteWhenfrom the probe rowregistry: compatibilityExceptions[3] has a blank or non-string 'deleteWhen'ownerfrom the probe rowregistry: compatibilityExceptions[3] has a blank or non-string 'owner'db.Expenses.AnyAsyncline (stale row)stale compatibility exception ...CurrencyBoundRowProbe.AnyAsync -> Finance ... (its trigger was #855)db.Expenses.AnyAsynctoPaymentRepository(new undeclared read)undeclared compatibility exception ...PaymentRepository.AnyExpenseAsync -> Finance at ...PaymentRepository.cs:22db.Expenses.Count()toListAccountsCliCommand(name walk over Api)undeclared compatibility exception Cluckwork.Api.Cli.ListAccountsCliCommand.RunAsync -> Finance at ...:30CompatibilityExceptionTests(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/mainat7ce95cf4. After merging #1015,origin/mainat4c429f08measures 595 Application tests; this PR adds no tests to the other three projects.origin/main4c429f08)dotnet build Cluckwork.slnhas 0 warnings and 0 errors (head716da3ff). 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
imagecheck no longer fails on CVE-2026-84782 (#1006). This PR does not touch the Dockerfile.Change map
Files this PR shares with other open work. It adds no production code change.
module-ledger.jsongains only the new section.SimulationDataSeederandCurrencyBoundRowProbeare untouched, so #851's farm-settings edits andIFarmModule.CanChangeCurrencyAsyncdo not collide.coupling-matrix.mdis unchanged. After #851 merges, the probe row keeps its key,CurrencyBoundRowProbe.AnyAsync, as long as the method keeps its name.