You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Nothing enforces that the eighth will. That is not a hypothetical: #300 exists only because #282
added the parameter to MultiFieldItemForm and not to its six siblings, and the asymmetry sat there
until someone filed an issue about it. #327 fixed the six by hand and added self-tests — but those
tests name today's seven builders one by one:
A builder added tomorrow appears in none of them, so it can ship without the parameter and the suite
stays green. The fix for #300 is therefore a snapshot, not an invariant — the same class of
gap #297 closed for the fixture's models and this issue closes for its builders.
This repo already has the idiom.CollectionItemShapeGuard (#297, PR #306) reflects over the test
assembly and fails the build when a suite re-declares a shape the fixture provides; FormCraft.UnitTests/Extensions/NativeRequiredBuilderTests.cs reflects over a builder surface; the FormCraft.UnitTests/Ci/* suites scan workflow source. A conformance guard here is the house style,
not a new mechanism.
Proposed solution
A guard mirroring CollectionItemShapeGuard's shape — a record offence type with a readable ToString(), a static class exposing FindOffenders(members, allowed), and a test class holding both
the [Fact] that runs it over the real fixture and the unit tests that prove the guard itself bites.
It asserts two things per builder, because the signature alone is only half the contract:
Shape — every IFormConfiguration<>-returning member of CollectionItemFixture declares a last, optional parameter of type Action<CollectionFieldBuilder<TModel, TItem>> whose TModel matches the member's own return type. Last and optional are load-bearing: they are
what let configureCollection exists on only one CollectionItemFixture builder #300 be purely additive, and a future builder that inserts the parameter anywhere else
silently rebinds every positional call site.
Behaviour — the callback actually reaches the collection and runs after the builder's own WithLabel/WithItemForm. Checkable without rendering: invoke the member reflectively with nulls
for every field callback and a configureCollection that sets a sentinel label, then assert the
returned configuration carries the sentinel. A builder that accepts the parameter and drops it, or
invokes it before its own label, fails.
Point 2 is what makes this worth building rather than a lint. AllowReorder() — the obvious probe — is
an order-insensitive setter, so it cannot distinguish "callback runs last" from "callback runs first";
overriding the label can only succeed if the caller runs last. #327 pinned that for the current seven
in Each_Item_Forms_Collection_Callback_Should_Run_After_The_Fixtures_Own_Configuration; the guard
generalises it to every future builder.
Scan the source text instead of reflecting (the FormCraft.UnitTests/Ci/* idiom). Rejected: those
suites scan YAML, where there is no type system to ask. Here the members are typed and loaded, so
reflection answers the question exactly, while a regex over .cs would re-implement C# parameter
parsing to reach a worse answer.
Assert only the signature, not the behaviour. Cheaper, and the natural first cut. Rejected as
half a guard: a builder can take configureCollection and never invoke it, which type-checks, passes
a signature guard, and silently gives every consuming suite an unconfigured collection.
Extend CollectionItemShapeGuard rather than adding a sibling. Rejected: that guard answers "does
any suite re-declare a model shape?" over types in the assembly; this one answers "is the fixture's
own builder surface uniform?" over members of one class. Merging them would give one class two
unrelated inputs and two unrelated offence vocabularies.
Area
FormCraft.ForMudBlazor.UnitTests — test infrastructure
⚠️Sequencing with #343. That issue proposes extracting the framework-agnostic half of CollectionItemFixture (models, factories, and the builders — everything that names no MudBlazor
type) into a shared test-support project so the Fluent UI suite can reach it. This guard reflects
over exactly those builders, so it moves with them. Neither blocks the other: land whichever comes
first and relocate the guard alongside the fixture. Worth doing #343 first if both are scheduled
together, to avoid writing the guard twice.
🧠 Brainstorm
Problem / context
CollectionItemFixture is the shared collection-field fixture for FormCraft.ForMudBlazor.UnitTests.
It exists because eleven suites each carried their own near-identical models and form configurations,
which drifted (#205, #258, #282). Its value is that every suite gets the same shape, so a change to
the render path is asserted once rather than eleven times.
That value depends on the builders being uniform. They are not uniform by construction — they are
seven hand-written static methods that happen to agree, and the project has already watched them stop
agreeing once. #297 recognised the same risk for the fixture's models and answered it with a build-time
guard rather than a convention; the builders' parameter surface got no such treatment.
The concrete surface at stake: TextItemForm, NumericItemForm, DateItemForm, BooleanItemForm, DecimalItemForm, MultiFieldItemForm, RootFieldAndItemForm.
Approaches
A. Reflection conformance guard over the fixture's members — signature and behaviour. Pros: answers the real question exactly (the members are typed and loaded); catches every future
builder for free; mirrors CollectionItemShapeGuard so a reader already knows the shape; the
behaviour half also generalises #327's invoke-order test, which currently enumerates by name. Cons: reflectively invoking a generic-delegate parameter needs a small MakeGenericMethod helper —
about fifteen lines of fiddly-but-standard code that itself wants a test.
B. Signature-only reflection guard. Pros: half the code, no reflective invocation, no helper. Cons: a builder that accepts the
parameter and never invokes it passes. That is a plausible mistake — it is what a copy-paste of the
signature without the body produces — and it fails silently in every consuming suite rather than
loudly here.
C. Source-text scan of CollectionItemFixture.cs. Pros: no reflection at all. Cons: re-implements C# parameter-list parsing with regexes to answer a
question the type system already knows; brittle against formatting; cannot check behaviour at all.
D. Do nothing; rely on review. Pros: free. Cons: this is the status quo that produced #300.
Recommendation
A. The behaviour half is the part that earns the issue — a signature guard alone would have caught #282's omission, but not the more likely future failure of a builder that declares the parameter and
forgets to invoke it. The reflective invocation is confined to one helper with its own negative
control, which is the same containment CollectionItemShapeGuard uses for its own fiddly parts.
📋 Spec
Goal
Adding a new item-form builder to CollectionItemFixture without a working configureCollection
callback fails the build, with a message naming the member and what is wrong.
Scope
A CollectionItemBuilderSurfaceGuard in FormCraft.ForMudBlazor.UnitTests/Fields/, exposing FindOffenders(IEnumerable<MethodInfo> members, IReadOnlySet<MethodInfo>? allowed = null).
A BuilderSurfaceOffence record with a readable ToString(), mirroring ShapeOffence.
CollectionItemBuilderSurfaceGuardTests — the [Fact] running the guard over the real fixture, plus
unit tests proving the guard flags each violation class and clears a conforming member.
Non-goals
Any production-code change. This is test infrastructure only.
flowchart TD
A["CollectionItemFixture members<br/>returning IFormConfiguration<>"] --> B{"last parameter"}
B -- "missing / not last / not optional" --> X["BuilderSurfaceOffence"]
B -- "wrong closed type<br/>(TModel mismatch)" --> X
B -- "conforms" --> C["invoke reflectively:<br/>nulls + sentinel WithLabel"]
C -- "sentinel absent" --> X
C -- "sentinel present" --> D["conforming"]
X --> E["[Fact] fails, naming member + reason"]
Loading
Validation rules
Shape offences, one per member: no trailing Action<CollectionFieldBuilder<,>>; the parameter
exists but is not last; it is not optional; its TModel does not match the member's IFormConfiguration<TModel> return type.
Behaviour offence: invoking the member with a configureCollection that calls WithLabel(sentinel) yields a configuration in which no field carries the sentinel — covering both
"callback dropped" and "callback invoked before the builder's own WithLabel".
The guard must be proven to bite. Non-conforming fakes live in a nested private type inside the
test class so CollectionItemShapeGuard does not flag them as re-declared shapes — nested types are
already classified as copies rather than shared shapes, so keep them nested and free of collection
properties, or add them to that guard's allowlist with a reason.
./build.sh Format clean; dotnet build -c Release --no-incremental → 0 warnings; ./build.sh Test
green.
Edge cases
RootFieldAndItemForm takes three callbacks, two of them field-level. "Last parameter" is the
rule, not "second parameter" — do not encode a position count.
NumericItemForm and BooleanItemForm share BasketModel.Lines. Their TModel is identical,
so a TModel-match check cannot tell them apart — that is fine, it is not trying to; but do not
"de-duplicate by model type" when enumerating members or one of the two vanishes from the guard.
Reflective invocation passes null for every field callback. Every builder already treats those
as optional (configure?.Invoke(…)), so this is safe — but a future builder that dereferences one
unconditionally would throw here rather than report an offence. Catch TargetInvocationException
and turn it into an offence with the inner exception's message, rather than letting the [Fact]
die with an unhelpable stack.
The sentinel must not collide with a real label. Use something obviously synthetic
("__configureCollection_sentinel__"), not "Renamed".
Non-builder members must be excluded.NewOrder, NewBasket, RenderItemForm etc. do not
return IFormConfiguration<>; filter on the return type, not on the name.
Assumptions
Base branch is dev. Test-only, so no version impact.
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: a build-time guard asserting every CollectionItemFixture item-form builder declares a
trailing optional configureCollection and actually invokes it last.
Architecture: test project only (FormCraft.ForMudBlazor.UnitTests). No production file may be
modified. Mirror CollectionItemShapeGuard's split — offence record + static guard with FindOffenders + a test class holding the real-fixture [Fact] and the guard's own unit tests.
Tech stack: .NET 10 test project (libraries multi-target net8.0;net10.0), xUnit v3 +
Microsoft.Testing.Platform, Shouldly. No bUnit needed — the behaviour check inspects the built IFormConfiguration<>, it does not render.
Global constraints:
Base branch dev; commit as Philippe Matray <phmatray@gmail.com>; conventional commits; PR title
ends (#<issue>) and is linted by pr-title-lint.yml.
Two independent CI gates since The .editorconfig's style rules are enforced nowhere, so 574 violations have accumulated #301 — ./build.cmd Format (wraps dotnet format --verify-no-changes, runs before Test) and ./build.cmd Test. Satisfy both. ⚠️dotnet formatapplying can write literal <<<<<<< TODO: Unmerged change from project …
blocks into .cs files on this multi-targeted repo; after any apply run grep -rl '<<<<<<< TODO' --include='*.cs' . and hand-resolve.
TreatWarningsAsErrors=true (NoWarn=CS1591;CS8620) — any compiler warning fails the build.
dotnet test --filter is inert (MTP0001). Per-class filtering runs the test host directly and
requires a prior dotnet build -c Release because it does not build: FormCraft.ForMudBlazor.UnitTests/bin/Release/net10.0/FormCraft.ForMudBlazor.UnitTests --filter-class <FQN>.
Add --list-tests --no-ansi --no-progress to count rather than run. Treat Zero tests ran (exit 8)
as a hard stop.
Record per-suite test counts before starting and assert each is unchanged after.
Task 1: The offence record and the shape half of the guard
Interfaces:record BuilderSurfaceOffence(MethodInfo Member, string Reason) with ToString(); internal static IReadOnlyList<BuilderSurfaceOffence> FindOffenders(IEnumerable<MethodInfo> members, IReadOnlySet<MethodInfo>? allowed = null); internal static IEnumerable<MethodInfo> ItemFormBuilders(Type fixture) filtering on an IFormConfiguration<> return type.
Step 1: Record the per-suite count for CollectionItemFixtureTests and the project total from dotnet test -c Release.
Step 2: Write failing unit tests for the shape rules against nested non-conforming fakes: no trailing callback; callback present but not last; callback not optional; callback's TModel mismatched. Plus a conforming fake that must yield no offence — the negative control without which the guard could return "everything is an offence" and pass.
Step 3: Run the per-class filter → FAIL (guard type does not exist).
Step 4: Implement BuilderSurfaceOffence and FindOffenders' shape checks, plus ItemFormBuilders filtering on the return type (never on the member name).
Step 5: Run the per-class filter → PASS.
Step 6: Commit: test(mudblazor): guard the item-form builders' collection-callback signature.
Task 2: The behaviour half — the callback is invoked, and invoked last
Interfaces: a private static bool ReachesCollection(MethodInfo member, out string? reason) building a closed Action<CollectionFieldBuilder<TModel, TItem>> via MakeGenericMethod, invoking the member with nulls elsewhere, and looking for the sentinel label on the built configuration.
Step 1: Write the failing tests: a fake that declares the parameter and never invokes it → offence; a fake that invokes it before its own WithLabel → offence (the sentinel is overwritten); a conforming fake → none.
Step 2: Run the per-class filter → FAIL.
Step 3: Implement the reflective invocation. Use an obviously synthetic sentinel ("__configureCollection_sentinel__"). Wrap the Invoke so a TargetInvocationException becomes an offence carrying the inner message rather than killing the run.
Step 4: Run the per-class filter → PASS.
Step 5: Commit: test(mudblazor): assert each builder actually invokes its collection callback last.
Task 3: Run it over the real fixture and document the contract
Step 1: Add the [Fact] running FindOffenders(ItemFormBuilders(typeof(CollectionItemFixture))) and asserting it is empty, with an Additional Info message telling a future author what to do — mirroring CollectionItemShapeGuardTests' wording.
Step 2: Run dotnet test -c Release → PASS with zero offenders across all seven builders; confirm every previously-recorded per-suite count is unchanged apart from the new tests.
Step 3: Mutation-check that the [Fact] bites: temporarily delete configureCollection from DecimalItemForm and confirm it goes red naming that member; restore and re-run green.
Step 4: Point CollectionItemFixture's class summary at the guard, so the ordering contract it documents names the test that enforces it.
Problem / motivation
#300 (PR #327) gave every item-form builder in
CollectionItemFixturea trailing optionalconfigureCollectioncallback. All seven now have one:Nothing enforces that the eighth will. That is not a hypothetical: #300 exists only because #282
added the parameter to
MultiFieldItemFormand not to its six siblings, and the asymmetry sat thereuntil someone filed an issue about it. #327 fixed the six by hand and added self-tests — but those
tests name today's seven builders one by one:
A builder added tomorrow appears in none of them, so it can ship without the parameter and the suite
stays green. The fix for #300 is therefore a snapshot, not an invariant — the same class of
gap #297 closed for the fixture's models and this issue closes for its builders.
This repo already has the idiom.
CollectionItemShapeGuard(#297, PR #306) reflects over the testassembly and fails the build when a suite re-declares a shape the fixture provides;
FormCraft.UnitTests/Extensions/NativeRequiredBuilderTests.csreflects over a builder surface; theFormCraft.UnitTests/Ci/*suites scan workflow source. A conformance guard here is the house style,not a new mechanism.
Proposed solution
A guard mirroring
CollectionItemShapeGuard's shape — arecordoffence type with a readableToString(), a static class exposingFindOffenders(members, allowed), and a test class holding boththe
[Fact]that runs it over the real fixture and the unit tests that prove the guard itself bites.It asserts two things per builder, because the signature alone is only half the contract:
IFormConfiguration<>-returning member ofCollectionItemFixturedeclares alast, optional parameter of type
Action<CollectionFieldBuilder<TModel, TItem>>whoseTModelmatches the member's own return type. Last and optional are load-bearing: they arewhat let configureCollection exists on only one CollectionItemFixture builder #300 be purely additive, and a future builder that inserts the parameter anywhere else
silently rebinds every positional call site.
WithLabel/WithItemForm. Checkable without rendering: invoke the member reflectively with nullsfor every field callback and a
configureCollectionthat sets a sentinel label, then assert thereturned configuration carries the sentinel. A builder that accepts the parameter and drops it, or
invokes it before its own label, fails.
Point 2 is what makes this worth building rather than a lint.
AllowReorder()— the obvious probe — isan order-insensitive setter, so it cannot distinguish "callback runs last" from "callback runs first";
overriding the label can only succeed if the caller runs last. #327 pinned that for the current seven
in
Each_Item_Forms_Collection_Callback_Should_Run_After_The_Fixtures_Own_Configuration; the guardgeneralises it to every future builder.
Alternatives considered
Finish adopting the collection-item fixture: five suites still copy its models under other names #282, and the miss survived long enough to become configureCollection exists on only one CollectionItemFixture builder #300. A convention that costs an issue every time
it is broken is not being enforced.
FormCraft.UnitTests/Ci/*idiom). Rejected: thosesuites scan YAML, where there is no type system to ask. Here the members are typed and loaded, so
reflection answers the question exactly, while a regex over
.cswould re-implement C# parameterparsing to reach a worse answer.
half a guard: a builder can take
configureCollectionand never invoke it, which type-checks, passesa signature guard, and silently gives every consuming suite an unconfigured collection.
CollectionItemShapeGuardrather than adding a sibling. Rejected: that guard answers "doesany suite re-declare a model shape?" over types in the assembly; this one answers "is the fixture's
own builder surface uniform?" over members of one class. Merging them would give one class two
unrelated inputs and two unrelated offence vocabularies.
Area
FormCraft.ForMudBlazor.UnitTests — test infrastructure
Follow-up from #327. Related: #300, #282, #297, #343
🧠 Brainstorm
Problem / context
CollectionItemFixtureis the shared collection-field fixture forFormCraft.ForMudBlazor.UnitTests.It exists because eleven suites each carried their own near-identical models and form configurations,
which drifted (#205, #258, #282). Its value is that every suite gets the same shape, so a change to
the render path is asserted once rather than eleven times.
That value depends on the builders being uniform. They are not uniform by construction — they are
seven hand-written static methods that happen to agree, and the project has already watched them stop
agreeing once. #297 recognised the same risk for the fixture's models and answered it with a build-time
guard rather than a convention; the builders' parameter surface got no such treatment.
The concrete surface at stake:
TextItemForm,NumericItemForm,DateItemForm,BooleanItemForm,DecimalItemForm,MultiFieldItemForm,RootFieldAndItemForm.Approaches
A. Reflection conformance guard over the fixture's members — signature and behaviour.
Pros: answers the real question exactly (the members are typed and loaded); catches every future
builder for free; mirrors
CollectionItemShapeGuardso a reader already knows the shape; thebehaviour half also generalises #327's invoke-order test, which currently enumerates by name.
Cons: reflectively invoking a generic-delegate parameter needs a small
MakeGenericMethodhelper —about fifteen lines of fiddly-but-standard code that itself wants a test.
B. Signature-only reflection guard.
Pros: half the code, no reflective invocation, no helper. Cons: a builder that accepts the
parameter and never invokes it passes. That is a plausible mistake — it is what a copy-paste of the
signature without the body produces — and it fails silently in every consuming suite rather than
loudly here.
C. Source-text scan of
CollectionItemFixture.cs.Pros: no reflection at all. Cons: re-implements C# parameter-list parsing with regexes to answer a
question the type system already knows; brittle against formatting; cannot check behaviour at all.
D. Do nothing; rely on review.
Pros: free. Cons: this is the status quo that produced #300.
Recommendation
A. The behaviour half is the part that earns the issue — a signature guard alone would have caught
#282's omission, but not the more likely future failure of a builder that declares the parameter and
forgets to invoke it. The reflective invocation is confined to one helper with its own negative
control, which is the same containment
CollectionItemShapeGuarduses for its own fiddly parts.📋 Spec
Goal
Adding a new item-form builder to
CollectionItemFixturewithout a workingconfigureCollectioncallback fails the build, with a message naming the member and what is wrong.
Scope
CollectionItemBuilderSurfaceGuardinFormCraft.ForMudBlazor.UnitTests/Fields/, exposingFindOffenders(IEnumerable<MethodInfo> members, IReadOnlySet<MethodInfo>? allowed = null).BuilderSurfaceOffencerecord with a readableToString(), mirroringShapeOffence.CollectionItemBuilderSurfaceGuardTests— the[Fact]running the guard over the real fixture, plusunit tests proving the guard flags each violation class and clears a conforming member.
Non-goals
CollectionItemFixture's builders — they already conform as of test(mudblazor): let every item-form builder configure its collection (#300) #327.named failure is easier to debug than a guard offence. The guard covers the general case; the
named tests cover the current one.
Surface
flowchart TD A["CollectionItemFixture members<br/>returning IFormConfiguration<>"] --> B{"last parameter"} B -- "missing / not last / not optional" --> X["BuilderSurfaceOffence"] B -- "wrong closed type<br/>(TModel mismatch)" --> X B -- "conforms" --> C["invoke reflectively:<br/>nulls + sentinel WithLabel"] C -- "sentinel absent" --> X C -- "sentinel present" --> D["conforming"] X --> E["[Fact] fails, naming member + reason"]Validation rules
Action<CollectionFieldBuilder<,>>; the parameterexists but is not last; it is not optional; its
TModeldoes not match the member'sIFormConfiguration<TModel>return type.configureCollectionthat callsWithLabel(sentinel)yields a configuration in which no field carries the sentinel — covering both"callback dropped" and "callback invoked before the builder's own
WithLabel".test class so
CollectionItemShapeGuarddoes not flag them as re-declared shapes — nested types arealready classified as copies rather than shared shapes, so keep them nested and free of collection
properties, or add them to that guard's allowlist with a reason.
devas of test(mudblazor): let every item-form builder configure its collection (#300) #327../build.sh Formatclean;dotnet build -c Release --no-incremental→ 0 warnings;./build.sh Testgreen.
Edge cases
RootFieldAndItemFormtakes three callbacks, two of them field-level. "Last parameter" is therule, not "second parameter" — do not encode a position count.
MultiFieldItemFormtakes five. Same rule; it is also the member whose parameter predates theconvention (Finish adopting the collection-item fixture: five suites still copy its models under other names #282), so it must not be special-cased or allowlisted.
NumericItemFormandBooleanItemFormshareBasketModel.Lines. TheirTModelis identical,so a
TModel-match check cannot tell them apart — that is fine, it is not trying to; but do not"de-duplicate by model type" when enumerating members or one of the two vanishes from the guard.
nullfor every field callback. Every builder already treats thoseas optional (
configure?.Invoke(…)), so this is safe — but a future builder that dereferences oneunconditionally would throw here rather than report an offence. Catch
TargetInvocationExceptionand turn it into an offence with the inner exception's message, rather than letting the
[Fact]die with an unhelpable stack.
(
"__configureCollection_sentinel__"), not"Renamed".NewOrder,NewBasket,RenderItemFormetc. do notreturn
IFormConfiguration<>; filter on the return type, not on the name.Assumptions
dev. Test-only, so no version impact.CollectionItemShapeGuardand moves with the fixture if The Fluent UI test suite re-declares the collection-item fixture's models #343 lands first.🛠️ Implementation plan
Goal: a build-time guard asserting every
CollectionItemFixtureitem-form builder declares atrailing optional
configureCollectionand actually invokes it last.Architecture: test project only (
FormCraft.ForMudBlazor.UnitTests). No production file may bemodified. Mirror
CollectionItemShapeGuard's split — offence record + static guard withFindOffenders+ a test class holding the real-fixture[Fact]and the guard's own unit tests.Tech stack: .NET 10 test project (libraries multi-target
net8.0;net10.0), xUnit v3 +Microsoft.Testing.Platform, Shouldly. No bUnit needed — the behaviour check inspects the built
IFormConfiguration<>, it does not render.Global constraints:
dev; commit asPhilippe Matray <phmatray@gmail.com>; conventional commits; PR titleends
(#<issue>)and is linted bypr-title-lint.yml../build.cmd Format(wrapsdotnet format --verify-no-changes, runs before Test) and./build.cmd Test. Satisfy both.dotnet formatapplying can write literal<<<<<<< TODO: Unmerged change from project …blocks into
.csfiles on this multi-targeted repo; after any apply rungrep -rl '<<<<<<< TODO' --include='*.cs' .and hand-resolve.TreatWarningsAsErrors=true(NoWarn=CS1591;CS8620) — any compiler warning fails the build.dotnet test --filteris inert (MTP0001). Per-class filtering runs the test host directly andrequires a prior
dotnet build -c Releasebecause it does not build:FormCraft.ForMudBlazor.UnitTests/bin/Release/net10.0/FormCraft.ForMudBlazor.UnitTests --filter-class <FQN>.Add
--list-tests --no-ansi --no-progressto count rather than run. TreatZero tests ran(exit 8)as a hard stop.
Task 1: The offence record and the shape half of the guard
Files: create
Fields/CollectionItemBuilderSurfaceGuard.cs,Fields/CollectionItemBuilderSurfaceGuardTests.cs.Interfaces:
record BuilderSurfaceOffence(MethodInfo Member, string Reason)withToString();internal static IReadOnlyList<BuilderSurfaceOffence> FindOffenders(IEnumerable<MethodInfo> members, IReadOnlySet<MethodInfo>? allowed = null);internal static IEnumerable<MethodInfo> ItemFormBuilders(Type fixture)filtering on anIFormConfiguration<>return type.CollectionItemFixtureTestsand the project total fromdotnet test -c Release.TModelmismatched. Plus a conforming fake that must yield no offence — the negative control without which the guard could return "everything is an offence" and pass.BuilderSurfaceOffenceandFindOffenders' shape checks, plusItemFormBuildersfiltering on the return type (never on the member name).test(mudblazor): guard the item-form builders' collection-callback signature.Task 2: The behaviour half — the callback is invoked, and invoked last
Files: modify
Fields/CollectionItemBuilderSurfaceGuard.cs,Fields/CollectionItemBuilderSurfaceGuardTests.cs.Interfaces: a private
static bool ReachesCollection(MethodInfo member, out string? reason)building a closedAction<CollectionFieldBuilder<TModel, TItem>>viaMakeGenericMethod, invoking the member with nulls elsewhere, and looking for the sentinel label on the built configuration.WithLabel→ offence (the sentinel is overwritten); a conforming fake → none."__configureCollection_sentinel__"). Wrap theInvokeso aTargetInvocationExceptionbecomes an offence carrying the inner message rather than killing the run.test(mudblazor): assert each builder actually invokes its collection callback last.Task 3: Run it over the real fixture and document the contract
Files: modify
Fields/CollectionItemBuilderSurfaceGuardTests.cs,Fields/CollectionItemFixture.cs(doc comment only).Interfaces: none new.
[Fact]runningFindOffenders(ItemFormBuilders(typeof(CollectionItemFixture)))and asserting it is empty, with anAdditional Infomessage telling a future author what to do — mirroringCollectionItemShapeGuardTests' wording.dotnet test -c Release→ PASS with zero offenders across all seven builders; confirm every previously-recorded per-suite count is unchanged apart from the new tests.[Fact]bites: temporarily deleteconfigureCollectionfromDecimalItemFormand confirm it goes red naming that member; restore and re-run green.CollectionItemFixture's class summary at the guard, so the ordering contract it documents names the test that enforces it.git diff --name-only dev...HEADlists no file outsideFormCraft.ForMudBlazor.UnitTests/../build.sh Format→ clean,dotnet build -c Release --no-incremental→ 0 warnings, then./build.sh Test→ green.test(mudblazor): fail the build when an item-form builder skips its collection callback.