diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md index 55bddf20..e50eeecd 100644 --- a/docs/release-notes/index.md +++ b/docs/release-notes/index.md @@ -16,6 +16,7 @@ Releases with new features, breaking changes, or bug fixes. | Version | Date | Highlights | |---------|------|------------| +| [v1.7.0](v1.7.0.md) | 2026-08-14 | **Fix**: `[Remote]` method bodies no longer ship to trimmed Blazor WASM clients. Two mechanisms: the registrar attribute's `[DynamicallyAccessedMembers]` named the consumer's own class (`[Execute]` static factories and `[FactoryEventHandler]` classes, since v0.21.2) — now a single-method forwarding holder; and the `IsServerRuntime` guard did not fold inside an `async` state machine (**every class factory with an async operation**) — now a non-async wrapper forwarding to a private core. **Feat**: positional-record DTOs, DTOs carried as `[Factory]` entity properties, and `FactoryEventBase` descendants reachable only from a subscription lambda are now preserved (DAM does **not** flow to derived types under ILLink). **Perf**: incremental caching restored on the relay-handler branch. **Behaviour change**: the server-only guard throws synchronously rather than as a faulted `Task`. Interface factories are explicitly carved out — body removal there is *not established*. | | [v1.6.1](v1.6.1.md) | 2026-05-24 | **Fix**: `[Factory]` classes whose only operation is `[Execute]` and whose ctor requires DI services were not registered as transients by the generator. Client-side deserialization could not resolve such types via `GetRequiredService`, so the ctor never ran and ctor-injected services stayed null. `requiresEntityRegistration` now also fires when `typeInfo.RequiresServiceInstantiation` is true. | | [v1.6.0](v1.6.0.md) | 2026-04-25 | **Feat**: new generator diagnostic **NF0106** — factory-operation attribute (`[Create]`/`[Fetch]`/`[Insert]`/`[Update]`/`[Delete]`/`[Execute]`) on a `[Factory]` interface method. Replaces cryptic CS0111/CS0738 duplicate-codegen errors with one clear diagnostic. Design project + skill gain interface-factory `[AuthorizeFactory]` pedagogy (`AuthorizedRepository.cs`, 10 tests, snippet-backed skill section) demonstrating the shipped Execute/Read + parameter-matching model. No breaking changes. | | [v1.5.0](v1.5.0.md) | 2026-04-14 | **Breaking**: entire `[Event]` method attribute API deleted — `EventAttribute`, `FactoryOperation.Event`, `AuthorizeFactoryOperation.Event`, `IEventTracker`, `IEventScopeInitializer`, `EventTrackerHostedService`, `AddRemoteFactoryEventScopeInitializer`, diagnostics NF0401-NF0404, log events 9001-9009. Consumers using `[Event]` for fire-and-forget work migrate to manual `Task.Run` + `IServiceScopeFactory.CreateScope()`. `[FactoryEventHandler]`, `IFactoryEvents`, `IFactoryEventRelay`, and `ICorrelationContext` are **unchanged**. | @@ -65,6 +66,7 @@ Releases with new features, breaking changes, or bug fixes. ## All Releases +- [v1.7.0](v1.7.0.md) - 2026-08-14 - **Fix**: `[Remote]` bodies stripped from trimmed clients for `[Execute]` static factories, `[FactoryEventHandler]` classes, and class factories with `async` operations (generated single-method registrar holders + non-async guard wrapper). **Feat**: positional-record DTOs, entity-property DTOs, and subscribe-only event records now preserved under IL trimming. **Perf**: relay-handler incremental caching. **Behaviour change**: server-only guard throws synchronously instead of returning a faulted `Task`. Interface-factory body removal remains **unestablished** and is not claimed. - [v1.6.1](v1.6.1.md) - 2026-05-24 - **Fix**: `[Execute]`-only `[Factory]` classes with DI-requiring ctors are now auto-registered as transients so client-side deserialization can invoke the ctor via DI (was missing in `FactoryServiceRegistrar`). - [v1.6.0](v1.6.0.md) - 2026-04-25 - **Feat**: NF0106 generator diagnostic for factory-operation attributes on interface factory methods (replaces cryptic CS0111/CS0738 duplicate-codegen errors). Interface-factory `[AuthorizeFactory]` pedagogy added to Design project (`AuthorizedRepository.cs`, 10 tests) and skill (snippet-backed section). - [v1.5.0](v1.5.0.md) - 2026-04-14 - **Breaking**: `[Event]` method attribute API fully deleted (attribute, `FactoryOperation.Event`, `AuthorizeFactoryOperation.Event`, `IEventTracker`, `IEventScopeInitializer`, `EventTrackerHostedService`, `AddRemoteFactoryEventScopeInitializer`, NF0401-NF0404, log events 9001-9009). Migrate fire-and-forget to manual `Task.Run` + `IServiceScopeFactory.CreateScope()`. `[FactoryEventHandler]` / `IFactoryEvents` / `IFactoryEventRelay` / `ICorrelationContext` unchanged. diff --git a/docs/release-notes/v1.0.0.md b/docs/release-notes/v1.0.0.md index 1ce99a27..10b2f555 100644 --- a/docs/release-notes/v1.0.0.md +++ b/docs/release-notes/v1.0.0.md @@ -3,7 +3,7 @@ layout: default title: "v1.0.0" description: "Release notes for Neatoo RemoteFactory v1.0.0" parent: Release Notes -nav_order: 5 +nav_order: 9 --- # v1.0.0 — Production Release diff --git a/docs/release-notes/v1.1.0.md b/docs/release-notes/v1.1.0.md index 5cc423a7..8ff10e1f 100644 --- a/docs/release-notes/v1.1.0.md +++ b/docs/release-notes/v1.1.0.md @@ -3,7 +3,7 @@ layout: default title: "v1.1.0" description: "Release notes for Neatoo RemoteFactory v1.1.0" parent: Release Notes -nav_order: 4 +nav_order: 8 --- # v1.1.0 — Transactional Factory Events diff --git a/docs/release-notes/v1.2.0.md b/docs/release-notes/v1.2.0.md index 6f855c81..915bc5b3 100644 --- a/docs/release-notes/v1.2.0.md +++ b/docs/release-notes/v1.2.0.md @@ -3,7 +3,7 @@ layout: default title: "v1.2.0" description: "Release notes for Neatoo RemoteFactory v1.2.0" parent: Release Notes -nav_order: 3 +nav_order: 7 --- # v1.2.0 — Trimming-Safe Factory Events diff --git a/docs/release-notes/v1.3.0.md b/docs/release-notes/v1.3.0.md index f065507d..e16ec46c 100644 --- a/docs/release-notes/v1.3.0.md +++ b/docs/release-notes/v1.3.0.md @@ -3,7 +3,7 @@ layout: default title: "v1.3.0" description: "Release notes for Neatoo RemoteFactory v1.3.0" parent: Release Notes -nav_order: 2 +nav_order: 6 --- # v1.3.0 — `CanSave(target)` Overload + Auth Method Triplication Fix diff --git a/docs/release-notes/v1.4.0.md b/docs/release-notes/v1.4.0.md index 86388c17..e4b4118f 100644 --- a/docs/release-notes/v1.4.0.md +++ b/docs/release-notes/v1.4.0.md @@ -3,7 +3,7 @@ layout: default title: "v1.4.0" description: "Release notes for Neatoo RemoteFactory v1.4.0" parent: Release Notes -nav_order: 2 +nav_order: 5 --- # v1.4.0 — Factory Event Relay Redesign + Post-Return Ordering Fix diff --git a/docs/release-notes/v1.5.0.md b/docs/release-notes/v1.5.0.md index 1ac1114a..ef9ba59d 100644 --- a/docs/release-notes/v1.5.0.md +++ b/docs/release-notes/v1.5.0.md @@ -3,7 +3,7 @@ layout: default title: "v1.5.0" description: "Release notes for Neatoo RemoteFactory v1.5.0" parent: Release Notes -nav_order: 2 +nav_order: 4 --- # v1.5.0 — Delete `[Event]` Method Attribute API diff --git a/docs/release-notes/v1.6.0.md b/docs/release-notes/v1.6.0.md index ddeec049..8f65fda9 100644 --- a/docs/release-notes/v1.6.0.md +++ b/docs/release-notes/v1.6.0.md @@ -3,7 +3,7 @@ layout: default title: "v1.6.0" description: "Release notes for Neatoo RemoteFactory v1.6.0" parent: Release Notes -nav_order: 2 +nav_order: 3 --- # v1.6.0 — NF0106 Diagnostic + Interface-Factory `[AuthorizeFactory]` Pedagogy diff --git a/docs/release-notes/v1.6.1.md b/docs/release-notes/v1.6.1.md index 950b036a..09b88fc5 100644 --- a/docs/release-notes/v1.6.1.md +++ b/docs/release-notes/v1.6.1.md @@ -3,7 +3,7 @@ layout: default title: "v1.6.1" description: "Release notes for Neatoo RemoteFactory v1.6.1" parent: Release Notes -nav_order: 1 +nav_order: 2 --- # v1.6.1 — `[Execute]`-only Factory Registration Fix diff --git a/docs/release-notes/v1.7.0.md b/docs/release-notes/v1.7.0.md new file mode 100644 index 00000000..04b665d1 --- /dev/null +++ b/docs/release-notes/v1.7.0.md @@ -0,0 +1,163 @@ +--- +layout: default +title: "v1.7.0" +description: "Release notes for Neatoo RemoteFactory v1.7.0" +parent: Release Notes +nav_order: 1 +--- + +# v1.7.0 — Trimming Preservation and Over-Preservation + +**Release Date:** 2026-08-14 +**NuGet:** [Neatoo.RemoteFactory 1.7.0](https://nuget.org/packages/Neatoo.RemoteFactory/1.7.0) +**Breaking changes:** None to the public API. One **behaviour change** in an error path — see below. + +--- + +## Overview + +This release closes both halves of RemoteFactory's IL-trimming story. + +**Under-preservation** — DTO shapes that the generator failed to preserve, so they were trimmed away and failed to deserialize on a Blazor WASM client. Positional records in factory signatures, plain DTOs reachable only as `[Factory]` entity properties, and `FactoryEventBase` descendants known only through a subscription lambda. + +**Over-preservation** — the inverse defect, and the more serious one. `[Remote]` method bodies were shipping to trimmed clients, decompilable, for **most** factory shapes. Every `[Execute]` static factory and `[FactoryEventHandler]` class since v0.21.2; and every class factory with an `async` operation — which is most aggregate roots — since the beginning. The headline promise that server-side SQL, business rules, and IP do not reach the browser was true for synchronous class-factory operations and false almost everywhere else. + +Both are fixed and both are now **measured** in a publish-trimmed artifact under a CI gate, rather than inferred from the shape of the generated code. The gate carries positive controls, so a missing or unreadable artifact fails loudly instead of passing silently. + +One shape is **not** covered — see [What this release does not claim](#what-this-release-does-not-claim). + +--- + +## What's New + +### Positional-record DTOs are preserved in factory signatures + +A record with only a parameterized constructor — the ordinary positional-record shape — appearing as a factory method's return type, as a parameter, or nested as a property of another discovered DTO, now survives publish-trimming with no consumer `LinkerConfig.xml` entry. + +The walker required a parameterless constructor before descending, so positional records were skipped with no preservation at all. `DtoConstructorRegistry.PreserveType()` already existed for exactly this shape but was emitted nowhere; the generator now emits it. + +### DTOs carried as `[Factory]` entity properties are discovered + +The walker stopped at `[Factory]`-annotated roots without descending into their properties, so a DTO reachable only as an entity property — for example a record carried by an `[Execute]`-opened aggregate — was trimmed on the client. The generator now walks the entity's property graph. + +### Event records are preserved without a handler on the client + +`FactoryEventBase` carries `[DynamicallyAccessedMembers]` with `Inherited = true`, and that was assumed to preserve descendants. It does not: **DAM does not flow to derived types under ILLink.** A `FactoryEventBase`-derived record whose only client-side reference was a subscription lambda was trimmed and failed to deserialize. + +The generator now emits a per-assembly **event-preservation registrar** that roots every concrete, accessible descendant and its nested property graph. This was verified with a red trimmed repro before the fix, not assumed. + +### Incremental-generator caching restored on the relay-handler branch (`perf`) + +`RelayHandlerModel` carried non-equatable collection fields, so its transform never compared equal and the relay-handler branch re-ran on every keystroke. Fixed, with a regression guard that pins the cached pipeline steps. + +--- + +## Bug Fixes + +### `[Remote]` bodies no longer ship to trimmed clients + +Two distinct mechanisms were leaking method bodies. Both needed fixing; neither fix alone is sufficient. + +**1. The registrar attribute named the consumer's own class.** + +`[assembly: NeatooFactoryRegistrar(typeof(T))]` carries `[DynamicallyAccessedMembers(PublicMethods | NonPublicMethods)]`, which preserves **every method on `T`, bodies included**. Static factories and `[FactoryEventHandler]` classes have no separate generated type — the generator re-opens the user's own partial class to host `FactoryServiceRegistrar` — so the attribute named the user's class and DAM retained all of it. + +The generator now emits a single-method forwarding holder and points the attribute at that: + +```csharp +internal static class NeatooFactoryRegistrar_MyCommands +{ + internal static void FactoryServiceRegistrar(IServiceCollection services, NeatooFactory remoteLocal) + => MyCommands.FactoryServiceRegistrar(services, remoteLocal); +} +``` + +Naming a *generated* type is necessary but **not sufficient** — what makes a holder safe is that it has exactly one method. Class factories therefore emit one too (`NeatooClassFactoryRegistrar_{ClassName}`) rather than naming `{X}Factory`, which hosts every `Local*` method for that factory. + +**2. The `IsServerRuntime` guard did not fold inside an `async` method.** + +In an `async` method the compiler lowers the whole body — guard included — into the state machine's `MoveNext`, inside the builder's protected region. ILLink folds the feature switch there but does not eliminate the unreachable remainder, so the body survived. A synchronous method puts the guard ahead of any protected region, which is why sync operations always trimmed correctly and `async` ones never did. + +The generator now emits the guard in a **non-async wrapper** that forwards to a private `async` core: + +```csharp +public Task LocalFetch(int id, CancellationToken cancellationToken = default) +{ + if (!NeatooRuntime.IsServerRuntime) + throw new InvalidOperationException("Server-only method called in non-server runtime."); + return LocalFetchCore(id, cancellationToken); +} + +private async Task LocalFetchCore(int id, CancellationToken cancellationToken = default) { /* ... */ } +``` + +Applied at all five guarded emission sites: reads, writes, `LocalSave`, `Can*`, and class-level `[Execute]`. + +**Affected shapes, now measured absent on a trimmed client:** `[Execute]` static factories, `[FactoryEventHandler]` classes, class factories with any `async` read or write operation, `LocalSave` routing, and class-level `[Execute]`. + +--- + +## Behaviour Changes + +**The server-only guard now throws synchronously.** + +Previously the guard lived inside the `async` body, so calling a server-only method on a non-server runtime produced a **faulted `Task`**. It now throws from the non-async wrapper, so it can reach the call site **synchronously**. + +- **Awaiting callers are unaffected.** `await factory.FetchAsync(id)` behaves identically, and `try { await … } catch (InvalidOperationException) { }` still catches it. +- Whether it reaches *your* call site synchronously depends on the entry point: non-async entry points (most reads) propagate it; `async` ones (`Save` on an authorized factory) capture it back into a faulted `Task`. +- **Only the server-only guard moved.** Authorization failures, target casts, and DI resolution failures still surface as faulted tasks in every case. + +The exception type and message are unchanged: `InvalidOperationException("Server-only method called in non-server runtime.")`. + +This fires only when a server-only method is invoked on a client runtime, which is already a misconfiguration — the boundary error you would want surfaced either way. + +--- + +## What this release does not claim + +**Interface factories are carved out.** `InterfaceFactoryRenderer` still emits the guard inline in the `async` body and still points its assembly attribute at `{ImplName}Factory` — it has received **neither** fix above. No leak is observed, but no proof is available either: the leg reaches its implementation through interfaces, so a client-side trimmed harness reads "absent" whether or not the body survives. + +Body removal on this leg is therefore **not established**. It is not asserted in the docs, the skill, the Design projects, or the generator's own comments. If you carry sensitive server-only logic in a `[Factory]` **interface** implementation, do not rely on it being trimmed from a WASM client in this release. + +--- + +## Migration Guide + +**No action required for the vast majority of consumers.** No public API changed; no attribute, type, or method signature was added, removed, or altered. Upgrading is a package-version bump. + +**If you consume RemoteFactory from a trimmed Blazor WASM client**, you may now be able to delete `LinkerConfig.xml` entries for types that flow through RemoteFactory — positional-record DTOs, DTOs carried as `[Factory]` entity properties, and `FactoryEventBase`-derived event records. Delete them, publish `Release`, and smoke-test; DTOs that reach the client through your own HTTP/JSON paths still need their entries. + +**If you split a factory call from its `await`**, review those sites. This pattern now surfaces the guard earlier: + +```csharp +// The exception now escapes at the call, not at the await. +var task = factory.FetchAsync(id); +try { await task; } catch (InvalidOperationException) { /* no longer reached */ } + +// Same for un-awaited tasks handed to a combinator: +await Task.WhenAll(factory.FetchAsync(a), factory.FetchAsync(b)); // throws before WhenAll +``` + +Wrap the call site rather than the await: + +```csharp +try { await factory.FetchAsync(id); } catch (InvalidOperationException) { /* still correct */ } +``` + +Again, this only applies when the method is server-only and the runtime is not the server. + +--- + +## Commits + +- `bed0651` — feat(generator): preserve positional-record DTOs in factory signatures (TRIM-001) +- `925ab0b` — feat(generator): discover DTOs carried as `[Factory]` entity properties (TRIM-002) +- `80762ed` — feat(generator): emit per-assembly event-preservation registrar (TRIM-007) +- `710498c` — perf(generator): restore incremental caching on the relay-handler branch (TRIM-006) +- `bd133e2` — fix(generator): point static-factory registrar attribute at a forwarding holder (TRIM-008) +- `c383530` — fix(generator): point relay-handler registrar attribute at a forwarding holder (TRIM-008) +- `c7cc680` — fix(generator): stop shipping async `[Remote]` bodies to trimmed clients (TRIM-009) + +Full engineering record, including the falsified hypotheses and the two abandoned diagnoses, is in the [TRIM todo container](https://github.com/NeatooDotNet/RemoteFactory/tree/main/docs/todos/TRIM-dto-trimming-preservation-gaps). + +See the [GitHub release](https://github.com/NeatooDotNet/RemoteFactory/releases/tag/v1.7.0) for the full commit list (auto-generated at release time). diff --git a/docs/todos/TRIM-dto-trimming-preservation-gaps/reviews/closeout-audit.md b/docs/todos/TRIM-dto-trimming-preservation-gaps/reviews/closeout-audit.md index 88e32af0..bdcc52e4 100644 --- a/docs/todos/TRIM-dto-trimming-preservation-gaps/reviews/closeout-audit.md +++ b/docs/todos/TRIM-dto-trimming-preservation-gaps/reviews/closeout-audit.md @@ -23,7 +23,7 @@ Every checkable finding was independently re-derived at the keyboard before bein **Queued as new rows:** C2 → row **23** (two `FactoryEventRelayTests` tests pass vacuously under exactly the condition that reddens their siblings — *a check that cannot go red, inside the class this arc has called a flake for five weeks*, which makes item 10's frequency an undercount by construction). C3 → item **11** widened (TRIM-009 routed its Step 7B to a row whose rationale does not cover it; `ClassFactoryWithExecute.cs` is the one Design source-of-truth file silent on the shape AC6 was held open for). -**Accepted with reason:** C7 (`attributes-reference.md` attributes `[Execute]` body removal to the guard alone — incomplete, not false, and it links to `trimming.md`), C8 (`TrimmingTests/README.md` "How It Works" predates the wrapper/holder — omission, to fix at the release step), C10 (AC6 measured on one TFM and one RID; same SDK therefore same ILLink, so the risk is small, but the wording is "every shape that can be measured" and the measurement is single-TFM). +**Accepted with reason:** C7 (`attributes-reference.md` attributes `[Execute]` body removal to the guard alone — incomplete, not false, and it links to `trimming.md`), C8 (`TrimmingTests/README.md` "How It Works" predates the wrapper/holder — omission, to fix at the release step; **done 2026-08-14**: rewritten as two independently-broken conditions A and B, with the holder-prefix table taken from the renderers by grep — the first draft named the relay prefix from memory and got it wrong — and an explicit note that the interface leg's checks are not evidence of body removal), C10 (AC6 measured on one TFM and one RID; same SDK therefore same ILLink, so the risk is small, but the wording is "every shape that can be measured" and the measurement is single-TFM). **Queued separately, pre-existing and outside arc scope:** C9 — `skills/RemoteFactory/references/polymorphic-hierarchy.md` cites a file in the private zTreatment repo, violating `CLAUDE.md`'s self-containment rule for the distributable skill. diff --git a/docs/todos/TRIM-dto-trimming-preservation-gaps/todo.md b/docs/todos/TRIM-dto-trimming-preservation-gaps/todo.md index 9cfe2ce1..06f5e40c 100644 --- a/docs/todos/TRIM-dto-trimming-preservation-gaps/todo.md +++ b/docs/todos/TRIM-dto-trimming-preservation-gaps/todo.md @@ -2,7 +2,7 @@ **ID:** TRIM **Type:** Generator defect / trimming-support completion -**Status:** In Progress +**Status:** Done (2026-08-14 — v1.7.0 released; AC1–AC4 + AC6 met, AC5 discharged per its close-out restatement) **Priority:** High (blocks a consumer from retiring a ~50-entry LinkerConfig.xml workaround) **Created:** 2026-07-06 @@ -25,9 +25,13 @@ A third suspected gap turned out to be already fixed: event records derive `Fact 2. A plain DTO reachable only as a public property of a `[Factory]` entity survives publish-trimming and deserializes on the client with no consumer LinkerConfig entry. [TRIM-002] 3. Verified (not assumed): a `FactoryEventBase`-derived record whose only client-side reference is a subscription-lambda call site deserializes on a publish-trimmed client. [TRIM-003] 4. `docs/trimming.md` ("What Qualifies as a DTO", "DTO Return Type Preservation") updated to match the shipped behavior; release notes per CI/CD standards. + + **Closed 2026-08-14.** `docs/trimming.md` was corrected across TRIM-008 and TRIM-009 (inventory at [`reviews/008-doc-anchor-inventory.md`](./reviews/008-doc-anchor-inventory.md)) and `docs/release-notes/v1.7.0.md` cut with the index, nav_order, and `Directory.Build.props` bump. The release notes carry the **behaviour change** that commit-prefix scanning would have dropped (row 22b) and an explicit *what this release does not claim* section for the interface-factory carve-out — so the published notes state the same boundary AC6 does, rather than letting a reader infer full coverage. 5. Consumer proof: released version consumed by zTreatment (PCB-003) with the LinkerConfig bulk-preserve block deleted and a Release WASM publish verified. (Tracked zTreatment-side; this todo closes on the framework release, not the consumer rollout.) **Restated at close-out (audit C4):** as written this bullet demanded consumer proof and then made itself non-binding in its own parenthetical, so it could neither be met nor honestly ticked. **AC5 is discharged by the framework release**; the consumer verification it describes belongs to zTreatment PCB-003 and is not a gate on this todo. Recorded so no future reader concludes the arc closed over an unmet criterion. + + **Discharged 2026-08-14** by the v1.7.0 cut. The LinkerConfig deletion and Release WASM publish remain zTreatment PCB-003's, now unblocked. Stated plainly: **this todo closes without consumer proof**, by the restatement above — not because consumer proof was obtained. 6. `[Remote]` method bodies and their server-only dependencies are absent from a publish-trimmed client for **every** factory shape that can be measured — `[Execute]` static factories, `[FactoryEventHandler]` classes, class factories with **any** async operation (read *and* write, both measured), `LocalSave` routing, and class-level `[Execute]` — proven in the trimmed harness, not inferred. [TRIM-008 + TRIM-009] **Closed as written 2026-08-14**, not narrowed. Class-level `[Execute]` was added to the criterion at TRIM-009's plan review rather than closed over: it is emitted `async` unconditionally, is a Design source-of-truth pattern, and had no harness coverage at all — so AC6's "proven, not inferred" was unsatisfiable for it. It now has a target and is measured absent. @@ -75,7 +79,13 @@ Execution order: 004 → 001 → 002 → 003 → 007 → 005 → 006 → 008 → ## Close-Out Audit -**2026-08-12 — Verdict: CONCERNS → addressed.** Full record: [`reviews/close-out-audit.md`](./reviews/close-out-audit.md). +Two passes. **The second is the whole-arc one** and was not linked from here until the release step — a container-integrity miss recorded rather than quietly fixed, since "the final audit is not discoverable from the todo" is exactly the kind of gap these audits exist to catch. + +**2026-08-14 (whole arc) — Verdict: CONCERNS → all 5 veto-tier closed.** Full record: [`reviews/closeout-audit.md`](./reviews/closeout-audit.md). + +Scope: the container in full (todo, 9 plans, 12 review files, 2 evidence sets), the generator/library/test/doc surface it touches, and 7 logs. V1 two integration tests red → closed by re-run (561+561, 0 failed, both TFMs). V2 `InterfaceFactoryRenderer.cs:260` still asserted body removal — the claim H1 measured insufficient — and this todo said the retraction landed in "all three" places when it had landed in five and missed that sixth. V3 release-step obligations untracked → row 22. V4 deferred item 4's own trigger had fired inside the arc → QUEUED. V5 TRIM-008 `Done` with 9/10 acceptance bullets unticked → ticked. Callouts: C1/C4/C5/C6 fixed inline; C2 → row 23; C3 → item 11 widened; **C8 (`TrimmingTests/README.md` "How It Works" predates the wrapper/holder) fixed at the release step, as its disposition said**; C7/C10 accepted with reason; C9 queued separately. + +**2026-08-12 (first pass) — Verdict: CONCERNS → addressed.** Full record: [`reviews/close-out-audit.md`](./reviews/close-out-audit.md). AC1–AC3 confirmed genuinely verified in a publish-trimmed artifact at HEAD (CI run 31658596913 at `3aed354`) — not a repeat of TRIM-003's assumed-preservation failure. Four veto-tier findings, all concentrated in TRIM-006 (the plan whose per-plan gates were folded into this audit) plus container bookkeeping; all four fixed: @@ -89,7 +99,7 @@ AC1–AC3 confirmed genuinely verified in a publish-trimmed artifact at HEAD (CI | # | Item | Destination | Cost if it stays open | |---|---|---|---| | 1 | **Registrar-DAM over-preservation** — `[Remote]` bodies for `[Execute]` and `[FactoryEventHandler]` classes ship to the browser decompilable | **[TRIM-008]** — folded into the arc 2026-08-12, reversing the plan-mode routing. Closes on that plan | Resolved: it now has the durable home the audit said it needed | -| 2 | **Release held (AC4 + AC5)** — version stays `1.6.1`, no v1.7.0 notes | **Unblocked 2026-08-14.** Item 1 merged (PR #75), item 18 closed by TRIM-009 (PR #76), close-out audit run. Nothing technical remains — see the release-step row (22) for what the cut must not drop | zTreatment PCB-003 blocked since July. The trade was re-affirmed twice at the keyboard: consumer unblock-time vs. publishing false IP guidance about the *most common* factory shape. AC6 is now satisfiable as written rather than by narrowing | +| 2 | **Release held (AC4 + AC5)** — version stays `1.6.1`, no v1.7.0 notes | **CLOSED 2026-08-14 — v1.7.0 cut.** `Directory.Build.props` at `1.7.0`, `docs/release-notes/v1.7.0.md` authored, index + 1.x `nav_order` chain renumbered; `v1.7.0` tagged off `main` once this merges, CI publishes. The release was outstanding from the todo's creation (2026-07-06) and **explicitly held from 2026-08-12**; in the two days of explicit hold it caught two further false claims (items 18 and 20) that shipping on 2026-08-12 would have published as truth | zTreatment PCB-003 blocked since July. The trade was re-affirmed twice at the keyboard: consumer unblock-time vs. publishing false IP guidance about the *most common* factory shape. AC6 is now satisfiable as written rather than by narrowing | | 3 | **`DiagnosticTestHelper` stale-generator hazard** — a generator fix can appear verified when it was never loaded; affects the whole generator suite | Documented at the seam (`DiagnosticTestHelper.cs`); durable fix (fail fast when the generator DLL predates the test assembly) explicitly not done | Local-iteration only (CI is cold-build). Already produced one false green during TRIM-006 | | 4 | **B8 — nothing pins the guard's runtime throw.** No `AppContext.SetSwitch` anywhere; `"Server-only method called in non-server runtime."` never asserted | **QUEUED 2026-08-14 — this row's own trigger fired inside the arc, and the row was not updated until the close-out audit caught it.** The acceptance said "queue if the guard's message or shape is ever edited"; TRIM-009 changed the shape (guard relocated from the async body into a non-async wrapper) **and** the observable semantics (synchronous throw instead of a faulted `Task`, escaping through `I{X}Factory` where the entry point is non-async). The gap is now larger than when it was accepted, not smaller | A regression deleting the throw ships silently in untrimmed/server scenarios. Root cause is that the `IsServerRuntime == false` path has never executed in any in-process test — no fixture sets the AppContext switch anywhere in the repo. One test-only switch fixture unlocks this row **and** the behaviour-change coverage the v1.7.0 release notes will describe | | 5 | **B10 — 16 emission assertions can pass vacuously.** `InternalVisibilityTests` / `CanMethodVisibilityTests` slice generated text with naive `IndexOf` bounded by the next member name | **Queued, unowned.** Not fixed here: out of TRIM-006's scope, and rewriting 16 assertions in sacred tests needs its own plan with its own review | False-green on the generated-code visibility contract — the same class of defect TRIM-001's test gate caught as its marquee finding | @@ -98,7 +108,7 @@ AC1–AC3 confirmed genuinely verified in a publish-trimmed artifact at HEAD (CI | 8 | **Falsified-doc inventory is incomplete** — also `docs/client-server-architecture.md:133` and `skills/RemoteFactory/references/trimming.md:14`. Conversely `skills/.../class-factory.md:318,333-334` and `advanced-patterns.md:227` were **over-listed** as falsified; they describe class factories, which trim correctly | **PARTIALLY CLOSED by TRIM-008** (2026-08-13). The inventory was rebuilt from the files rather than copied, and is recorded at [`reviews/008-doc-anchor-inventory.md`](./reviews/008-doc-anchor-inventory.md). It found this row's own premise wrong: class factories do **not** trim correctly for async operations (item 18), so the anchors this row calls "over-listed" are in fact false today. They are deferred to TRIM-009 rather than edited, because TRIM-009 makes them true and nothing ships before it lands — enumerated in the inventory as a release-blocking checklist. **CLOSED 2026-08-14:** TRIM-009 landed, so the deferred anchors are true of shipped behaviour. Its own doc pass then introduced two fresh falsehoods — `docs/trimming.md:236` (class factories "have a type to name that is not yours", contradicting `:249` eleven lines below) and `CLAUDE-DESIGN.md:760` ("**Every** factory shape therefore emits its own forwarding holder", which the interface leg does not) — both caught at code review and fixed | Closed. The recurring lesson held once more: TRIM-009's Step 7b list was built from the plan rather than from the files, and missed `docs/trimming.md:236` eleven lines from an anchor it did edit. Enumerate by reading the file, every time | | 9 | **`MethodParameterInfo.Equals` over-caching** (`FactoryGenerator.Types.cs:851-856`) compares only `Name`/`Type`, ignoring `IsService`/`IsTarget`/`IsCancellationToken`/`IsParams` | **Queued, unowned.** Pre-existing (`b1a93de`) | Adding/removing `[Service]` on a same-name-and-type parameter yields an equal `TypeInfo` → stale generated code. Inverse hazard to TRIM-006's: too-permissive equality makes the guard *greener*, never redder, so the new guard cannot see it | | 10 | **Relay-timing coverage loss** — `RelayTimingTests.cs:56,:105` skipped; wider `FactoryEventRelay` family flakes under parallel load | User declined queueing twice | Real coverage loss on relay continuation timing | -| 11 | **Design-project demonstration gap** — no `Design.Domain`/`Design.Tests` example exercises entity-carried DTO discovery (TRIM-002) or event preservation (TRIM-007); only comments were added | **Accepted with reason:** preservation is not observable in untrimmed Design tests; `RemoteFactory.TrimmingTests` is the real verification surface. Revisit at the release step. **Widened 2026-08-14 (close-out audit C3):** TRIM-009 routed its Step 7B here, but this row's rationale — "not observable in untrimmed Design tests" — does **not** transfer to TRIM-009, whose change is *behavioural*, not preservation-only. Concretely, `Design.Domain/FactoryPatterns/ClassFactoryWithExecute.cs` carries no trimming, holder, or wrapper commentary at all, while TRIM-008 gave `AllPatterns.cs` a full block including an explicit "not demonstrated by a Design test, and why". Class-level `[Execute]` is the shape AC6 was held open for, and it is the one Design source-of-truth file silent about it | Repo `CLAUDE.md` names the Design projects the requirements surface, so the gap is worth an explicit decision rather than silence. The TRIM-009 half is the cheaper and more valuable one: a commentary block mirroring `AllPatterns.cs:356-376` | +| 11 | **Design-project demonstration gap** — no `Design.Domain`/`Design.Tests` example exercises entity-carried DTO discovery (TRIM-002) or event preservation (TRIM-007); only comments were added | **Accepted with reason:** preservation is not observable in untrimmed Design tests; `RemoteFactory.TrimmingTests` is the real verification surface. Revisit at the release step. **Widened 2026-08-14 (close-out audit C3):** TRIM-009 routed its Step 7B here, but this row's rationale — "not observable in untrimmed Design tests" — does **not** transfer to TRIM-009, whose change is *behavioural*, not preservation-only. Concretely, `Design.Domain/FactoryPatterns/ClassFactoryWithExecute.cs` carries no trimming, holder, or wrapper commentary at all, while TRIM-008 gave `AllPatterns.cs` a full block including an explicit "not demonstrated by a Design test, and why". Class-level `[Execute]` is the shape AC6 was held open for, and it is the one Design source-of-truth file silent about it. **TRIM-009 half CLOSED 2026-08-14 at the release step:** `ClassFactoryWithExecute.cs` now carries the commentary block — both mechanisms, why `async` was the exposed shape, why the holder alone is insufficient (DAM covers `NonPublicMethods`), the synchronous-throw behaviour, and an explicit "not demonstrated by a Design test, and why". **The TRIM-002 / TRIM-007 half stays accepted-with-reason** — those are preservation-only and genuinely unobservable in untrimmed Design tests, which is the rationale that does transfer | Repo `CLAUDE.md` names the Design projects the requirements surface, so the gap is worth an explicit decision rather than silence. The TRIM-009 half is the cheaper and more valuable one: a commentary block mirroring `AllPatterns.cs:356-376` | | 12 | **`LazyLoad` spurious `Register>` emission** | **Accepted**, TRIM-002 Amendment — idempotent and harmless; removal needs its own trimmed verification | Cosmetic registrar noise | | 13 | **Interface-factory implementation classes get no property walk** | **Accepted by design**, TRIM-002 Constraint; documented at `docs/trimming.md:285` | A consumer serializing state off such a class still needs manual preservation | | 14 | **Nested `[Factory]` static / `[FactoryEventHandler]` classes emit uncompilable code** — simple-name FQN in the assembly attribute plus a namespace-scope re-declaration of the user's class | **Recorded, not fixed** (user decision 2026-08-12). Surfaced during TRIM-008 design; pre-existing | Rare shape, but the failure is a confusing cascade of CS errors in *generated* code. A cheap NF01xx diagnostic (`IsNested` is already computed) would make it one clear message | @@ -109,7 +119,8 @@ AC1–AC3 confirmed genuinely verified in a publish-trimmed artifact at HEAD (CI | 19 | **`[Service]` parameters on interface-factory methods emit uncompilable code (CS0535)** — the generator strips the service parameter from the proxy's implementing method while the `[Factory]` interface still declares it, so the emitted factory does not implement its own interface | **Recorded, not fixed.** Found 2026-08-13 during TRIM-008's re-review while trying to give the async interface-factory target a directly-reachable marker. Pre-existing; nothing in the repo, tests, or Design projects uses the shape, which is why it was never caught | Rare shape, but the failure is a CS error in *generated* code with no diagnostic pointing at the cause. It also means the interface-factory leg **cannot** carry a server-only marker in its generated body, so that leg is structurally unable to measure body-fold behaviour | | 20 | **The interface-factory leg shares BOTH mechanisms TRIM-009 fixes and receives neither** — `InterfaceFactoryRenderer` emits `Local*` with the same inside-the-async guard, and still points its assembly attribute at `{ImplName}Factory`, so DAM covers every `Local*` on it | **Queued, unowned.** Found at TRIM-009 plan review (2026-08-14, finding A4). Deliberately not taken into TRIM-009: it would balloon a plan whose arc the user has already flagged as over-running, and item 19 makes the leg structurally unmeasurable from a client-side harness | Deferring the *work* is fine; shipping a *false claim* is not. The skill asserted "Interface factory \| Yes" for body removal and `docs/trimming.md:37` called interface bodies "unreachable to the trimmer" — a claim the TRIM-008 inventory had already downgraded to "left standing because nothing contradicts it, not because it was measured". **Both qualified 2026-08-14**, plus `CLAUDE-DESIGN.md`; the `docs/trimming.md` half was missed by TRIM-009's first doc pass and caught at code review (V2). The claim now reads "not established" in all three, so the *work* is deferred without a false claim shipping. The remaining exposure is that the leg keeps the exact shape TRIM-009 measured insufficient — an inside-the-async guard plus a DAM target that hosts every `Local*` | | 21 | **`Local{X}Core` has no name-collision guard** — a factory method whose `UniqueName` ends in `Core` produces a wrapper `Local{Y}Core(...)` that can collide with the generated core of a method named `{Y}`. Identical parameter lists give CS0111 in *generated* code with no diagnostic pointing at the cause | **Queued, unowned.** Found at TRIM-009 code review (C4). No such shape exists in the repo, the Design projects, or the examples | Same family as items 14 and 15 — a CS error in generated code with no diagnostic. Rides along if a plan is ever cut for those; not worth one alone | -| 22 | **Release-step obligations, enumerated so the cut cannot drop them.** (a) Nine live artifacts describe `v1.7.0` behaviour in the past tense while `src/Directory.Build.props` is `1.6.1` and no `docs/release-notes/v1.7.0.md` exists — and two of them (`docs/trimming.md`'s holder-mechanism sections) are in **undated present tense** on the published Jekyll site, so a v1.6.1 consumer is told their `[Execute]` bodies are protected by a holder that ships in no installable version. (b) TRIM-009's **synchronous-throw behaviour change** lives only in a Done plan's prose; its commit is `fix:`-prefixed, so `CLAUDE.md`'s commit-scanning release process would emit a patch-flavoured bug-fix line and omit it entirely | **Queued for the release step** — created by the close-out audit (V3, 2026-08-14) | The container tracked the *inverse* exposure exhaustively (docs behind the code) and never tracked this one (docs ahead of the package). Item 2 has already slipped a month with nothing watching this window | +| 22 | **Release-step obligations, enumerated so the cut cannot drop them.** (a) Nine live artifacts describe `v1.7.0` behaviour in the past tense while `src/Directory.Build.props` is `1.6.1` and no `docs/release-notes/v1.7.0.md` exists — and two of them (`docs/trimming.md`'s holder-mechanism sections) are in **undated present tense** on the published Jekyll site, so a v1.6.1 consumer is told their `[Execute]` bodies are protected by a holder that ships in no installable version. (b) TRIM-009's **synchronous-throw behaviour change** lives only in a Done plan's prose; its commit is `fix:`-prefixed, so `CLAUDE.md`'s commit-scanning release process would emit a patch-flavoured bug-fix line and omit it entirely | **CLOSED 2026-08-14 by the v1.7.0 cut.** (a) discharged by the release itself — every `v1.7.0` reference (`docs/trimming.md`, `skills/.../trimming.md`, `CLAUDE-DESIGN.md`, `AllPatterns.cs`, `ClassFactoryWithExecute.cs`, `FactoryAttributes.cs`) now names a version that exists on NuGet. Enumerated by grep at the keyboard, not from this row's own count, which said "nine artifacts" and was counting something else. (b) carried into `docs/release-notes/v1.7.0.md` as its own **Behaviour Changes** section plus a Migration Guide entry for the split-call/await and `Task.WhenAll` shapes — exactly the content commit-prefix scanning would have emitted as a one-line patch fix. The notes also carry a *what this release does not claim* section for the interface-factory carve-out, which no commit prefix would have produced at all | The container tracked the *inverse* exposure exhaustively (docs behind the code) and never tracked this one (docs ahead of the package). Item 2 has already slipped a month with nothing watching this window | +| 24 | **`docs/release-notes/` `nav_order` collisions in the v0.x block.** Almost every v0.x page sits at `2`, and `v0.29.0` / `v0.7.0` sit at `1` — so they collide with the current release and Jekyll falls back to alphabetical title sort, which orders the archive wrongly | **Queued, unowned.** Pre-existing, and explicitly declared out of scope once before (`d8369b3`, "the wider v0.x nav_order overlap is pre-existing tech debt"). The v1.7.0 cut renumbered the **1.x chain** to a strict 1–9 rather than adding a fresh collision at the top, but did not touch v0.x | Cosmetic, but it recurs at every release: `CLAUDE.md` step 5 says "increment existing release page nav_orders", which taken literally is a 47-file edit nobody has ever done, so each cut either re-collides or quietly deviates. One renumber plus a one-line change to the documented process retires it permanently | | 23 | **Two tests in `FactoryEventRelayTests` pass vacuously under exactly the condition that reddens their siblings.** `ServerOnlyEvent_ExcludedFromRelayBatch` and `ServerOnlyCombinedFlags_NotRelayed` wait on `InvocationCount == 1`, then assert only `Assert.Empty(relay.Received)` — so when the relay never fires, the wait burns its 2s deadline and the assertion passes on an empty queue | **Queued, unowned** — created by the close-out audit (C2, 2026-08-14). Pre-existing; pairs with item 10 | **A check that cannot go red, inside the class this arc has called a flake for five weeks.** It means 2 of 7 tests there structurally cannot report relay non-delivery, so item 10's "~3 of 8 runs" frequency is an undercount by construction. Cheap fix: `Assert.Equal(1, relay.InvocationCount)` in both | ## Discovery Log @@ -286,3 +297,11 @@ AC1–AC3 confirmed genuinely verified in a publish-trimmed artifact at HEAD (CI - **A test was removed rather than kept.** An async-guarded `Can*` emission test was added to close the one changed site with no assertion — but `[AuthorizeFactory]` returning `Task` yields a Can that is async and **not** server-only, so no guard and no split: it would have passed for the wrong reason. The shape needs `[AspAuthorize]`, whose references the unit harness lacks. Removed, with the reason recorded at the test file and the real coverage (Design.Domain `SecureOrder`, AspNetCore TestLibrary) named. - **Five Test Evidence claims were overstated and are corrected**, including a size comparison against a number that turned out to be a knob variant rather than HEAD. That comparison is **withdrawn** rather than restated from memory, because no artifact records HEAD's pre-fix trimmed size. - **Final state:** 614+614 unit, 561+561 integration (5 pre-existing skips), 86+86 Design, harness exit 0, gate exit 0 with 10 named positive controls and six new per-site discriminators. + +### 2026-08-14 — v1.7.0 cut; the arc closes +- **The release is the deliverable AC4 named, and the release notes are where the arc's honesty either survives or evaporates.** `CLAUDE.md`'s documented process scans commit prefixes; run literally it would have produced three `feat:` lines, one `perf:` line, and three `fix:` lines — and **silently omitted the behaviour change**, because TRIM-009's commit is `fix:`-prefixed and prefix-scanning has no way to know a bug fix changed observable semantics. Deferred row 22 existed for exactly this and was written by the close-out audit two days earlier. It worked: the notes carry a **Behaviour Changes** section and a Migration Guide covering the split-call/await and `Task.WhenAll` shapes. +- **The published notes state the boundary rather than letting a reader infer coverage.** A *What this release does not claim* section names the interface-factory carve-out in the consumer-facing artifact — not just in the todo, the generator comments, and the skill. A release announcing "`[Remote]` bodies no longer ship to trimmed clients" without it would have been technically defensible and practically misleading, which is the failure mode AC6 was held whole to avoid. +- **The same doc-list failure recurred, and was caught by grep this time.** The `TrimmingTests/README.md` rewrite (audit callout C8) included a holder-prefix table written from memory: the relay prefix is `NeatooEventHandlerRegistrar_`, not `NeatooRelayHandlerRegistrar_`. Six occurrences in this arc of a claim assembled from recollection rather than from the artifact. The only countermeasure that has ever worked is running the grep before writing the sentence — not resolving to be more careful. +- **Two container-integrity gaps closed at the last moment.** The 2026-08-14 whole-arc audit was never linked from `todo.md`'s Close-Out Audit section (only the 2026-08-12 pass was), so the arc's final review was undiscoverable from its own container; and `reviews/` holds both `close-out-audit.md` and `closeout-audit.md`, two different passes under near-identical names. The first is fixed here; the second is left as-is rather than renamed, because both are cited by name from committed prose. +- **`nav_order` was renumbered for the 1.x chain only** (strict 1–9), not the 47-file archive. `CLAUDE.md` step 5 taken literally is a full renumber nobody has ever performed, so every cut either re-collides or quietly deviates; that is now row 24 rather than a sixth silent deviation. +- **AC5 closes without consumer proof, and says so.** The restatement at audit C4 made the framework release the discharge condition. Recorded plainly so no later reader concludes the arc obtained a verification it did not. diff --git a/src/Design/Design.Domain/FactoryPatterns/ClassFactoryWithExecute.cs b/src/Design/Design.Domain/FactoryPatterns/ClassFactoryWithExecute.cs index 14d1a8d9..b86fee4f 100644 --- a/src/Design/Design.Domain/FactoryPatterns/ClassFactoryWithExecute.cs +++ b/src/Design/Design.Domain/FactoryPatterns/ClassFactoryWithExecute.cs @@ -65,6 +65,41 @@ internal Task Create(string name, [Service] IExampleService service) /// - Interface method: IClassExecuteDemoFactory.RunCommand(string input) /// - Factory implementation with Local/Remote method pair /// - Delegate for remote execution + /// - A forwarding holder, NeatooClassFactoryRegistrar_ClassExecuteDemo, which the + /// assembly-level [NeatooFactoryRegistrar] attribute points at + /// + /// TRIMMING: class-level [Execute] is emitted `async` unconditionally, which made it + /// the shape most exposed by the two defects fixed in v1.7.0. + /// + /// 1. The guard now lives in a NON-ASYNC wrapper forwarding to a private core: + /// public Task<ClassExecuteDemo> LocalRunCommand(...) + /// { + /// if (!NeatooRuntime.IsServerRuntime) throw new InvalidOperationException(...); + /// return LocalRunCommandCore(...); + /// } + /// private async Task<ClassExecuteDemo> LocalRunCommandCore(...) { ... } + /// Inside an `async` method the compiler lowers the whole body -- guard included -- + /// into the state machine's MoveNext, within the builder's protected region. ILLink + /// folds the feature switch there but does not eliminate the unreachable remainder, + /// so this body shipped to trimmed Blazor WASM clients, decompilable. A synchronous + /// method puts the guard ahead of any protected region, which is why sync operations + /// always trimmed correctly and `async` ones did not. + /// + /// 2. The registrar attribute's [DynamicallyAccessedMembers] preserves every method on + /// the type it names, bodies included. Naming the GENERATED type is necessary but not + /// sufficient: {X}Factory hosts every Local* method, so class factories emit a + /// single-method holder rather than naming the factory directly. DAM covers + /// NonPublicMethods, so the holder alone would still root the private core -- both + /// halves are required. + /// + /// BEHAVIOUR (v1.7.0): the server-only guard throws synchronously from the wrapper + /// rather than surfacing as a faulted Task. Awaiting callers are unaffected. + /// Authorization failures, target casts, and DI resolution failures still surface as + /// faulted tasks -- only the server-only guard moved. + /// + /// Not demonstrated by a Design test: over-preservation is only observable in a + /// publish-trimmed artifact, and Design.Tests run untrimmed. RemoteFactory.TrimmingTests + /// is the verification surface; this shape is measured absent there. /// [Remote, Execute] public static async Task RunCommand( diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8e7e1bab..028fa638 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -15,8 +15,8 @@ enable net9.0;net10.0 True - 1.6.1 - 1.6.1 + 1.7.0 + 1.7.0 neatoo_icon.png LICENSE OO Domain Modeling C# .NET Blazor WPF ASP.NET CSLA diff --git a/src/Tests/RemoteFactory.TrimmingTests/README.md b/src/Tests/RemoteFactory.TrimmingTests/README.md index 55e57669..61352bf1 100644 --- a/src/Tests/RemoteFactory.TrimmingTests/README.md +++ b/src/Tests/RemoteFactory.TrimmingTests/README.md @@ -14,12 +14,49 @@ comes with a non-zero exit — new checks must follow that contract (append to ## How It Works +The trimmer removes a `[Remote]` body only when **both** of the following hold. Each was +broken independently before v1.7.0, and each is measured separately by the gate. + +**A. The guard folds, and the remainder is unreachable.** + 1. `TrimTestEntity` has a `[Remote, Create]` method with a `[Service] IServerOnlyRepository` parameter -2. The generated factory's `LocalCreate` method is guarded by `if (!NeatooRuntime.IsServerRuntime) throw ...` +2. The generated `Local*` method is guarded by `if (!NeatooRuntime.IsServerRuntime) throw ...` 3. The project sets `IsServerRuntime=false` via `RuntimeHostConfigurationOption` with `Trim="true"` 4. When published with `PublishTrimmed=true`, the trimmer constant-folds `IsServerRuntime` to `false` 5. Everything after the throw guard becomes dead code and is removed +Step 5 holds only for a **synchronous** method. In an `async` method the compiler lowers the +whole body — guard included — into the state machine's `MoveNext`, inside the builder's +protected region; ILLink folds the switch there but does not eliminate the unreachable +remainder. Since v1.7.0 the generator emits `async` guards in a **non-async wrapper** +(`LocalX`) forwarding to a private `async` core (`LocalXCore`), so the guard again sits ahead +of any protected region. The gate asserts the state machines themselves are absent +(`d__`, `d__`, …) precisely so a regression here cannot hide +behind an absent marker string. + +**B. Nothing else roots the body.** + +`[assembly: NeatooFactoryRegistrar(typeof(T))]` carries +`[DynamicallyAccessedMembers(PublicMethods | NonPublicMethods)]`, which preserves **every** +method on `T` — bodies included, private ones included. Naming a *generated* type is +necessary but not sufficient: `{X}Factory` hosts every `Local*` method for its factory, and +`NonPublicMethods` covers the private core from A. So each leg emits a **single-method +forwarding holder** and the attribute names that instead: + +| Leg | Holder prefix | +|---|---| +| Class factory | `NeatooClassFactoryRegistrar_` | +| Static factory (`[Execute]`) | `NeatooFactoryRegistrar_` | +| `[FactoryEventHandler]` | `NeatooEventHandlerRegistrar_` | + +The gate carries a positive control for each holder, so "absent" cannot be an artifact of the +holders never having been emitted. + +**Interface factories have neither fix.** That leg still guards inline in the `async` body and +still names `{ImplName}Factory`. It reaches its implementation through interfaces, so this +client-side harness reads "absent" whether or not the body survives — the leg is structurally +unmeasurable here and its checks are **not** evidence of body removal. + ## Running the Verification ```bash