Skip to content

feat(generator): preserve positional-record DTOs in factory signatures (TRIM-001) - #69

Merged
keithdv merged 5 commits into
mainfrom
TRIM-001-record-preservation
Jul 6, 2026
Merged

feat(generator): preserve positional-record DTOs in factory signatures (TRIM-001)#69
keithdv merged 5 commits into
mainfrom
TRIM-001-record-preservation

Conversation

@keithdv

@keithdv keithdv commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Second plan of the TRIM todo. Positional records in factory method signatures were silently dropped by DTO discovery — WalkFactoryReturn required a public parameterless ctor, so a record like zTreatment's StartVisitResultV2 (returned from [Remote, Execute]) got no preservation and the publish-trimmed client threw DeserializeNoConstructor. This was the largest contributor to zTreatment's ~50-entry LinkerConfig.xml workaround (PCB-003).

Changes

  • Two-bucket discovery (DtoTypeWalker.WalkDtoGraph): every discovered signature type — roots and nested alike — buckets by ctor shape: parameterless → Register<T>(() => new T()) (unchanged), parameterized-only → PreserveType<T>() (DAM-All rooting; deserialization flows through the existing RecordBypassConverterFactory). Applies to return types, non-service parameters, and property descent (which now enters record graphs). The dead WalkEventRoot is retired into the shared walk.
  • Pipeline threading: the preserve bucket rides as EquatableArray<string> on the transform-output records (TypeInfo/MethodInfo) — the incremental-cache boundary — per plan review; all three registrar renderers (class/interface/static) emit both call kinds.
  • Tests: 11 new RecordDtoDiscoveryTests (return/parameter/nested both directions/collection/mixed-ctor/record struct/cross-method dedupe/exclusions, across all three renderer paths) + RecordDtoSmokeTest in the trimmed CI harness. The harness records are never constructed in harness code, so their ctors are rooted solely by the emitted PreserveType — verified by two-stage negative controls (emission disabled → harness exits 1 on the record checks).
  • Docs: docs/trimming.md ("What Qualifies as a DTO" no longer conflates RecordBypassConverterFactory with preservation), CLAUDE-DESIGN.md registry section/criteria table/FAQ, AllPatterns.cs ExampleRecordResult remarks.

Reviews (in docs/todos/TRIM-dto-trimming-preservation-gaps/reviews/)

  • Plan review: APPROVED, 5 callouts — all folded in (001-plan-review.md)
  • Test gate: CLEARED, no open must/should-cover; caught and fixed a vacuous-coverage trap in the harness design (001-test-review.md)
  • Code review: no veto findings; one pre-existing low-confidence callout routed to TRIM-002 (001-code-review.md)

Test plan

  • Full solution: 572+572 unit, 563+563 integration, 0 failed (net9.0 + net10.0)
  • Trimmed harness green locally (win-x64), negative controls verified
  • This PR's CI run exercises the trimming gate on linux-x64

🤖 Generated with Claude Code

keithdv and others added 5 commits July 6, 2026 14:31
… full

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aft)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s (TRIM-001)

DtoTypeWalker's factory walk required a public parameterless ctor, so
positional records in factory signatures got no trimming preservation —
a publish-trimmed client threw DeserializeNoConstructor on them (the
zTreatment StartVisitResultV2 cut-over failure).

Discovered types now bucket-sort by ctor shape at every level of the walk:
parameterless -> DtoConstructorRegistry.Register<T>(() => new T()),
parameterized-only -> DtoConstructorRegistry.PreserveType<T>() (rooting
only; deserialization flows through RecordBypassConverterFactory). The
bucket threads through the incremental pipeline as EquatableArray on the
transform-output records; all three registrar renderers emit both calls.
The dead WalkEventRoot walker is retired into the shared bucketed walk.

- Unit: RecordDtoDiscoveryTests pins bucket assignment across return,
  parameter, nested (both directions), collection, mixed-ctor, interface/
  class/static renderer paths, and the unchanged exclusions
- Trimmed harness: RecordDtoSmokeTest deserializes records never
  constructed in client code (negative control verified: disabling the
  emission fails the harness with exit 1)
- Docs: trimming.md, CLAUDE-DESIGN.md, AllPatterns.cs updated to the
  two-bucket contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cord-struct + dedupe coverage

The gate caught false trimmed-harness coverage: _ProcessRecord's
constructed body rooted the record ctors (guarded-dead bodies are
retained on the client, the TRIM-005 behavior), so the return/nested
checks passed even with PreserveType emission disabled. The harness
method now returns null — discovery is signature-based — and two-stage
negative controls prove each shape depends solely on the emission.

Adds RecordStruct_LandsInRegisterBucket and
SameRecordFromTwoMethods_SinglePreserveTypeEmission; queues the
pre-existing incremental-cache test hole as TRIM-006.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cope

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@keithdv
keithdv merged commit e0588f7 into main Jul 6, 2026
3 of 4 checks passed
@keithdv
keithdv deleted the TRIM-001-record-preservation branch July 6, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant