From bd7b366509072617d2785686e021b9e9a34f0ec1 Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Mon, 12 Oct 2026 12:12:28 +0200 Subject: [PATCH 1/2] docs: clarify consumer release boundaries and installation Separate published 3.0.0 guidance from unreleased fixes, correct standalone and central package installation, expose analyzer diagnostic properties, and record upgrade effects. Keep the consumer skill focused on supported usage. Co-authored-by: Codex --- CHANGELOG.md | 14 ++- IMPLEMENTATION_PLAN.md | 7 ++ README.md | 16 ++-- STATUS.md | 8 ++ docs/USAGE.md | 85 +++++++++++++------ skills/mammoth-litemapper/SKILL.md | 12 +-- .../references/mapping-rules.md | 69 ++++++++++----- 7 files changed, 147 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe34364..d74bdaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,17 @@ ## Unreleased -## Fixes +The following changes are not included in the published `3.0.0` package. No next release version is assigned here. + +### Fixed - Issue #51: set mappings use the destination default comparer after custom element conversion, including same-type converters, preventing incorrect collapse of distinct mapped values. Identity-mapped sets retain compatible source comparers. [#51](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/51) - Issue #48: configured enum paths preserve missing reference intermediates as null for nullable enum destinations, including constructor-bound and existing-target mappings. By-name errors, by-value conversion, single evaluation, patch skipping, and converter precedence are preserved. [#48](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/48) +- Issue #47: configured direct and dotted nullable-value patch captures retain their source type when invoking nested updaters, preserving the selected nullable overload when a competing non-nullable overload exists. [#47](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/47) - Issue #32: recursive cycle paths retain compact immutable segments and format the full path only when a cycle is detected, removing quadratic path-prefix allocations while preserving exception metadata and first-edge allocation behavior. [#32](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/32) - Issue #31: dictionary mappings preserve compatible key comparers independently of value conversion. Keys transformed by custom converters use the destination default comparer, even when key types match. [#31](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/31) - Issue #30: set mappings probe exact accessible HashSet capacity constructors and use portable fallbacks when absent, including on netstandard2.0. Capacity-plus-comparer support is detected independently. [#30](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/30) +- Issue #29: patch-mode nested updaters consume the captured direct source value once, preserving null skips, overload selection, and sequential update behavior. [#29](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/29) - Issue #28: IgnoreCase rejects multiple case-insensitive source matches even when one is exact; nested structural ambiguity retains LITEMAPPER1004. Exact and ExactThenIgnoreCase keep their existing exact-match priority. [#28](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/28) - Issue #27: by-name enum exceptions use stable source names and paths instead of applying `nameof` to guarded expressions; nullable enum captures retain source metadata. [#27](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/27) - Issue #26: structural collection mapping preserves nullable elements and applies Throw guards before object helpers; nullable nested collection helper signatures retain their annotations. [#26](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/26) @@ -20,6 +24,12 @@ - Issue #20: release validation and each NuGet package push now stop immediately on a nonzero native exit, reporting the failed operation or package before later successes can conceal it. [#20](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/20) - Issue #19: tag-selected manual workflow dry runs no longer enter the NuGet publish job. Automatic tag publishing requires a push event; manual branch publishing retains its explicit publish and dry-run gates. [#19](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/19) +### Upgrade considerations + +- #28: ambiguous `IgnoreCase` mappings that compiled in `3.0.0` can now fail with `LITEMAPPER1004`. Select the intended source with `MapProperty.Source`, or use `Exact`/`ExactThenIgnoreCase` when exact-name priority is intentional. +- #31: corrected dictionary comparer selection can change key lookup and collision behavior. Value-only conversion now preserves a compatible source key comparer; custom key conversion uses the destination default comparer. Verify lookups and duplicate-key exceptions. +- #51: corrected set comparer selection can change membership and element counts after custom conversion. Values distinct under the destination default comparer are retained; duplicates under that comparer still collapse. Verify membership and collision expectations before upgrading. + ## 3.0.0 ### Breaking Changes @@ -37,8 +47,6 @@ ### Fixed -- Issue #29: patch-mode nested updaters consume the captured direct source value once, preserving null skips, overload selection, and sequential update behavior. [#29](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/29) - - Issue #11: existing-target mappings with `NullableMismatch.Throw` now evaluate and enforce nullable preconditions in deterministic per-member order, preserving earlier assignments when a later member fails. [#11](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/11) - Issue #12: declared cycle tracking now identifies mappings by symbol identity instead of method name, preventing overload false positives, invalid tracker arguments, and unnecessary tracking state. [#12](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/12) - Issue #13: direct nullable sources passed to nested updaters are evaluated once, preventing duplicate source getter or path evaluation. [#13](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/13) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index a87c013..bd2c449 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -1,5 +1,12 @@ # Mammoth.LiteMapper Implementation Plan +## Consumer documentation release boundaries (2026-10-12) + +- Scope: correct README/usage/consumer-skill installation and version boundaries, analyzer-property exports, and unreleased changelog placement. Preserve published `3.0.0` installation; no product source, public API, specification semantics, or package version changes. +- Plan: distinguish tagged `3.0.0` from the unreleased f0d4bee snapshot, pin supporting source links, document central package management separately, and explain upgrade effects for ambiguity and comparer fixes. Keep consumer guidance focused on recipes, configuration, diagnostics, and compatibility. +- Validation: run stable-package quickstart and analyzer-option probes, central-package restore/build, skill format/discovery/install checks where available, source-backed usage validation, and a scoped diff/line-ending review. Final PR-SHA release validation is tracked separately. +- Operations: isolated cloud worktree `codex-consumer-release-docs`; AgentStack CLI is absent and the authorized connector fallback omits issue comments/claim writes. Stop at draft PR/human review; no merge, tag, package publication, or workflow dispatch. + ## Issue #51: comparer selection after set element conversion (2026-10-12) - Scope: use the destination default comparer whenever set-element resolution selects a non-identity conversion, including same-type custom converters. Preserve identity comparer reuse, constructor capability selection, collection copying, single enumeration, null policy, and converter precedence. No public API or specification semantics change. diff --git a/README.md b/README.md index 5ace309..19adae9 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,7 @@ Mammoth.LiteMapper is a convention-first, compile-time object mapping library fo Consumers declare partial mapping methods on classes marked with `[LiteMapper]`. A Roslyn incremental source generator emits direct C# implementations using constructors, assignments, loops, casts, and ordinary method calls. LiteMapper is not a runtime mapping container and does not provide runtime mapper registration, assembly scanning, reflection fallback, or dynamic dispatch. -Assembly-wide defaults and external mapper registrations are compile-time inputs. When they change, the incremental generator reevaluates affected generated mappings and diagnostics while preserving unrelated output where Roslyn can preserve its candidate identity. - -Generated nested and collection helpers are private implementation details of each generated mapper. Their names are allocated deterministically within that mapper, reserve handwritten and other generated helper signatures, reuse names for identical closed source/destination pairs with equivalent effective options, nullable shape, and execution context, and disambiguate distinct semantic identities without depending on syntax-tree order. Consumers must not depend on generated helper names or signatures. +Assembly-wide defaults and external mapper registrations are compile-time configuration. Rebuild after changing them. Generated helpers are private; call only the mapping methods you declare and do not depend on helper names or signatures. ## Compilation Status @@ -16,12 +14,14 @@ Generated nested and collection helpers are private implementation details of ea ## Quickstart -Install the primary package: +Install the published stable primary package, `3.0.0`, in a project without central package management: ```xml - + ``` +For central package management, put the version in `Directory.Packages.props` instead; see [installation](docs/USAGE.md#installation). The [version notes](docs/USAGE.md#versions-and-upgrading) distinguish `3.0.0` from unreleased fixes in this checkout. + Declare a mapper: ```csharp @@ -53,10 +53,10 @@ Ordinary unmapped target members are reported as `LITEMAPPER1001` errors by defa The [mammoth-litemapper skill](skills/mammoth-litemapper/SKILL.md) provides consumer mapping guidance for compatible coding agents using the [Skills CLI](https://skills.sh/docs). -Once this skill is published on the repository's default branch, install it from your consuming project: +Install the skill from the explicit `develop` branch, which includes guidance for `3.0.0` and clearly marked unreleased corrections: ```sh -npx skills add PrimordialCode/Mammoth.LiteMapper --skill mammoth-litemapper +npx skills add https://github.com/PrimordialCode/Mammoth.LiteMapper/tree/develop/skills/mammoth-litemapper --skill mammoth-litemapper ``` -Append `--agent codex` to target Codex or `--global` for user-level installation. To preview this checkout's discoverable skills without installing, run `npx skills add . --list` from the repository root. The skill contains its own supporting reference; consumers do not need a LiteMapper source checkout. Local validation does not establish a skills.sh directory listing. +Append `--agent codex` to target Codex or `--global` for user-level installation. To preview this checkout without installing, run `npx skills add ./skills/mammoth-litemapper --list` from the repository root. The skill includes its supporting reference and needs no source checkout. Installing the skill does not upgrade the NuGet package; verify the package version before using an unreleased correction. diff --git a/STATUS.md b/STATUS.md index b4f381d..85fcde7 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,5 +1,13 @@ # Mammoth.LiteMapper Status +## Consumer documentation release-boundary checkpoint (2026-10-12) + +- Scope: documentation only, based on develop f0d4bee in isolated `codex-consumer-release-docs`. Stable `3.0.0` installation remains available; unreleased behavior and upgrade effects are explicit, upstream source links are pinned, central package management is separate, and analyzer options include compiler-visible exports. Changelog #29 is under Unreleased, #47 is included, and #28/#31/#51 have upgrade considerations. No package version, source, public API, or specification semantics changed. +- Consumer validation: five isolated C# 9/net10.0 consumers extracted from the documented snippets restore, build with zero warnings/errors, and execute `Id=42` against the published `3.0.0` packages: standalone, central package management, hidden/visible analyzer properties, and central management with visible analyzer properties. The versionless non-central negative control fails with NU1015. Generated source and analyzer config prove the exports enable both diagnostic comments and expose all three properties. +- Source-backed validation: explicit solution restore and Release packaging-test build pass with zero build warnings/errors. `dotnet test tests/Mammoth.LiteMapper.Packaging.Tests/Mammoth.LiteMapper.Packaging.Tests.csproj -c Release --no-build --no-restore --filter Milestone16UsageDocumentationTests` passes 1/1 with zero skips and runs the Basic, Collections, and ASP.NET Core smoke samples. +- Skill validation: skill-creator `quick_validate.py`, local discovery, isolated Codex installation including the supporting reference, and remote discovery through the documented develop URL pass. Local paths/anchors and pinned source-file links resolve; unchanged content retains its original line endings, and CRLF-aware diff checks pass. Remote discovery validates the available develop skill, not publication of this uncommitted correction. +- Environment and boundary: existing SDK 10.0.401, task-local caches/temporary files, and one-processor builds. AgentStack CLI is absent; authorized connector fallback omits issue comments/claim writes. Final PR-SHA CI, full release validation, and controlled performance evidence remain separately pending. Stop at draft PR/human review; no merge, tag, publication, or workflow dispatch. + ## GitHub issue #51 set element comparer checkpoint (2026-10-12) - Base: isolated cloud worktree from develop f7de4cf28ec63c2c3ac4c812cbdbe16502b2be62. Duplicate searches found no existing issue for the set-conversion defect; #30 and #31 concern separate constructor/dictionary behavior. New issue #51 has no blocked-by dependencies, sub-issues, or competing PR. Authorized exec:agent and ready:agent labels were applied and read back. AgentStack CLI is absent; connector fallback is used without issue comments or claim writes. diff --git a/docs/USAGE.md b/docs/USAGE.md index 7afb43f..9be045e 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -8,23 +8,51 @@ Runnable examples: - `samples/Mammoth.LiteMapper.Samples.Collections/Program.cs` - `samples/Mammoth.LiteMapper.Samples.AspNetCore/Program.cs` +## Versions and upgrading + +The stable package example installs `3.0.0`. This checkout also documents unreleased corrections implemented at [f0d4bee](https://github.com/PrimordialCode/Mammoth.LiteMapper/commit/f0d4beedc1c0f0105c0e2a5c37c283969ac23f96); they are not included in that package. The next package version has not been assigned here. For the released snapshot, see the [3.0.0 usage guide](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/docs/USAGE.md) and [3.0.0 samples](https://github.com/PrimordialCode/Mammoth.LiteMapper/tree/3.0.0/samples). + +Unreleased paragraphs below identify affected cases. In particular: + +- `IgnoreCase` will reject multiple case-insensitive member matches even if one is exact. Code that compiled under `3.0.0` may require an explicit source selection or a different matching policy (#28). +- Dictionary comparer preservation will depend on the selected key conversion, independently of value conversion. Recheck lookup and duplicate-key behavior (#31). +- Custom set element converters will use the destination default comparer even for the same element type. Recheck membership, element counts, and duplicate collapse (#51). +- Nullable enum source paths, captured patch updaters, nullable structural elements, and portable set construction have additional fixes. Check the [unreleased changelog](../CHANGELOG.md#unreleased) before relying on these shapes in `3.0.0`. + +Installing a newer skill or reading this branch's guide does not change the installed generator. Build and test the affected mappings against the package you actually reference. + ## Installation For coding-agent assistance, use the repository's [mammoth-litemapper skill](../skills/mammoth-litemapper/SKILL.md). See [agent skill installation](../README.md#agent-skill) for the Skills CLI command. Installing the skill does not install the NuGet package in your application. -Normal consumers reference the primary package only: +Normal consumers reference the primary package only. Without central package management, include the stable version: ```xml - + ``` +With NuGet central package management, enable it and declare the version in `Directory.Packages.props`: + +```xml + + + true + + + + + +``` + +Then use `` inside the consuming project's `ItemGroup`. A versionless reference without a central version fails restore; do not copy the repository's centrally managed reference into a standalone project. + `Mammoth.LiteMapper.Abstractions` is pulled in as a dependency and contains the public attributes, enums, and `LiteMapperCycleException`. The generator is included as an analyzer asset and is not a runtime dependency. ## Mapper declarations A generated mapper is a partial method inside a class marked with `[LiteMapper]`. -Apply `[LiteMapper]` once across the mapper's partial declarations. Other parts may contain mapping methods, `[UseMapper]` registrations, or unrelated attributes. LiteMapper combines those parts into one generated file. Repeating `[LiteMapper]` is invalid and reports `LITEMAPPER0013`. The Basic sample includes a separate partial part with `[DebuggerDisplay]`. +Unreleased (#24): Apply `[LiteMapper]` once across the mapper's partial declarations. Other parts may contain mapping methods, `[UseMapper]` registrations, or unrelated attributes. LiteMapper combines those parts into one generated file. Repeating `[LiteMapper]` is invalid and reports `LITEMAPPER0013`. The Basic sample includes a separate partial part with `[DebuggerDisplay]`. From `samples/Mammoth.LiteMapper.Samples.Basic/Program.cs`: @@ -54,7 +82,7 @@ A containing type can also be a partial record, record struct, or interface. For Instance mappers may use fields, properties, constructor-injected dependencies, and instance converter methods. Static mapper classes may use static converters only. -Automatic nested and collection mappings keep access to the same mapper instance when a selected converter or mapping method needs it. Helpers without instance-dependent calls remain static. A static mapping method declared inside an instance mapper can use only static converters and mapping methods. +Unreleased (#22): Automatic nested and collection mappings keep access to the same mapper instance when a selected converter or mapping method needs it. Helpers without instance-dependent calls remain static. A static mapping method declared inside an instance mapper can use only static converters and mapping methods. The [Basic sample](../samples/Mammoth.LiteMapper.Samples.Basic/Program.cs) passes a prefix into `InstanceMapper`: `new InstanceMapper("first:")` formats nested child value `7` as `"first:7"` and collection child value `8` as `"first:8"`. A separate `new InstanceMapper("second:")` uses its own prefix. User-owned mutable state remains the consumer's thread-safety responsibility. @@ -158,7 +186,7 @@ Configuration resolves from the most specific scope to the least specific scope: 3. `[assembly: LiteMapperDefaults(...)]` for assembly-level defaults. 4. Library defaults. -Assembly-level defaults and external mapper registrations are compile-time configuration inputs. When either changes during an incremental build, LiteMapper reevaluates affected generated mappings and diagnostics; unrelated output can remain cached where Roslyn preserves the relevant candidate identity. This affects build invalidation only and does not create a runtime mapper registry. +Assembly-level defaults and external mapper registrations are compile-time configuration. Rebuild after changing them so generated mappings and diagnostics reflect the new values. ```csharp [assembly: LiteMapperDefaults( @@ -233,13 +261,13 @@ Useful options: - `NameMatching.IgnoreCase`: unique ordinal ignore-case match, without exact-match priority. - `UnmappedMemberPolicy.Ignore`, `Info`, `Warning`, `Error`: controls diagnostics for unmapped source or target members. -If a source exposes both `Value` and `value`, automatic matching to target `Value` under `IgnoreCase` reports `LITEMAPPER1004` and omits the invalid implementation, including in updates and nested structural mappings. `Exact` and `ExactThenIgnoreCase` select the exact `Value` member. An explicit `MapProperty.Source` can select the intended member. Constructor parameters follow the same matching policy. +Unreleased (#28): If a source exposes both `Value` and `value`, automatic matching to target `Value` under `IgnoreCase` reports `LITEMAPPER1004` and omits the invalid implementation, including in updates and nested structural mappings. `Exact` and `ExactThenIgnoreCase` select the exact `Value` member. An explicit `MapProperty.Source` can select the intended member. Constructor parameters follow the same matching policy. Member candidates are public instance properties and fields. Hidden members are resolved to the most-derived usable member, with property-over-field preference when needed. Source paths in `[MapProperty]` may use dotted member paths such as `"Email.Value"`. Source paths must resolve to members and cannot use method-call syntax. Source methods are not discovered automatically; use a converter or root-source `MapProperty.Use` method when a method-derived value is needed. -Omitting `Source` with an explicit `Use` passes the complete root object to that converter. This selection takes priority over automatic name matching, so an unrelated source member with the target name is not read and no same-name source member is required. The same rule applies to constructor-bound members and existing-target mappings, including patch mode. +Unreleased (#23): Omitting `Source` with an explicit `Use` passes the complete root object to that converter. This selection takes priority over automatic name matching, so an unrelated source member with the target name is not read and no same-name source member is required. The same rule applies to constructor-bound members and existing-target mappings, including patch mode. The [Basic sample](../samples/Mammoth.LiteMapper.Samples.Basic/Program.cs) maps `StaticSource.Name == "Ada"` to `StaticTarget.DisplayName == "Name: Ada"`: @@ -627,7 +655,7 @@ Visible handwritten or generated mapping methods for the same nested pair are re Nested helpers are closed over the concrete source/destination type pair. Closed generic model types can be mapped when the containing mapper and mapping method are not generic. Nullable nested objects follow the same nullability policy as other members. -Nested and collection helpers are private implementation details within the generated mapper. Helper names are allocated deterministically per mapper: handwritten members and other generated helper signatures are reserved, identical closed source/destination pairs share helpers only when their effective options, relevant nullable shape, and execution context are equivalent, and distinct semantic identities are disambiguated even when their preferred shape-plus-hash names collide. Allocation does not depend on syntax-tree order or equivalent compilation order. Consumer code must not reference or depend on helper names or signatures. +Nested and collection helpers are private implementation details. Consumer code must not reference or depend on their names or signatures. Unreleased correction (#21): nested and collection mappings consistently retain each entry method's effective options and nullable shape; test mappings that share model types but use different policies when upgrading. For existing-target nested objects, LiteMapper replaces writable nested targets by default. Mutation of an existing nested object requires an explicit compatible existing-target nested mapping method. @@ -653,7 +681,7 @@ For a writable non-null child, select the updater explicitly with `[MapProperty( Omitting `Source` in `[MapProperty(Target = nameof(Target.Child), Use = nameof(ApplyChild))]` passes the root source to `void ApplyChild(Source source, ChildTarget target)`. No source member named `Child` is required: the updater can copy root `Value == 3` into `target.Child.Value`. -The patch guard and nested updater share one captured source value: a nullable getter is evaluated once, even if another read would return a different object or null. The [Basic sample](../samples/Mammoth.LiteMapper.Samples.Basic/Program.cs) demonstrates this with `PatchUpdaterExample`. Nullable value captures retain the selected updater overload for automatic direct members and configured direct or dotted source paths. The [configured overload sample](../samples/Mammoth.LiteMapper.Samples.Basic/ConfiguredUpdaterOverloadExample.cs) selects `ApplyChild(ConfiguredUpdaterValue?, ConfiguredUpdaterChild)` even when a competing non-nullable overload exists, and preserves the target for a null leaf or intermediate. +Unreleased (#29, #47): The patch guard and nested updater share one captured source value: a nullable getter is evaluated once, even if another read would return a different object or null. The [Basic sample](../samples/Mammoth.LiteMapper.Samples.Basic/Program.cs) demonstrates this with `PatchUpdaterExample`. Nullable value captures retain the selected updater overload for automatic direct members and configured direct or dotted source paths. The [configured overload sample](../samples/Mammoth.LiteMapper.Samples.Basic/ConfiguredUpdaterOverloadExample.cs) selects `ApplyChild(ConfiguredUpdaterValue?, ConfiguredUpdaterChild)` even when a competing non-nullable overload exists, and preserves the target for a null leaf or intermediate. Patch mode also applies to nested updater calls. If `source.Child` is null and `IgnoreNullSourceMembers` is enabled, the updater is skipped and the existing target child remains unchanged. With the default mismatch policy, passing a nullable child to a non-null updater parameter reports `LITEMAPPER2001`; `Throw` raises `InvalidOperationException` whose message includes the `Child` path. @@ -702,13 +730,15 @@ Supported collection mapping includes: - nested collections and collections of nested objects; - dictionary key and value conversion. -Nullable structural elements preserve null without calling an object-mapping helper. In the Collections sample, `MapNullableLines` maps a three-element source containing null, a line with quantity 7, and null to three elements with the same null positions and mapped quantity. This also applies to dictionary values and nested collections. Mapping a nullable element to a non-null target reports `LITEMAPPER2003` under `NullableMismatchPolicy.Error`; `Throw` checks the element before mapping and raises `InvalidOperationException` with its source element path. `NullCollectionStrategy.Empty` applies to the collection itself and does not replace or remove null object elements. +Unreleased (#26): Nullable structural elements preserve null without calling an object-mapping helper. In the Collections sample, `MapNullableLines` maps a three-element source containing null, a line with quantity 7, and null to three elements with the same null positions and mapped quantity. This also applies to dictionary values and nested collections. Mapping a nullable element to a non-null target reports `LITEMAPPER2003` under `NullableMismatchPolicy.Error`; `Throw` checks the element before mapping and raises `InvalidOperationException` with its source element path. `NullCollectionStrategy.Empty` applies to the collection itself and does not replace or remove null object elements. -LiteMapper enumerates arbitrary enumerable sources once. Capacity is preallocated only when cheap count or length information and the exact accessible constructor are available. Set mappings use parameterless or comparer-only construction on targets such as `netstandard2.0` where capacity overloads are absent. Comparers are preserved for compatible set and dictionary shapes when element or key mapping uses language identity. Unsupported shapes include custom collections, immutable collections, queues, stacks, and rectangular multidimensional arrays. +LiteMapper enumerates arbitrary enumerable sources once. Unsupported shapes include custom collections, immutable collections, queues, stacks, and rectangular multidimensional arrays. -The [portable set example](../samples/Mammoth.LiteMapper.Samples.Collections/PortableSetExample.cs) maps arrays and lists to `HashSet` and `ISet`, collapsing duplicate converted values. It also verifies detached identity-mapped copies with preserved case-insensitive comparers. A selected custom element converter uses the destination default comparer even when its input and output types match: mapping `A`, `B`, and `C` to `x`, `X`, and `x` retains both `x` and `X` while collapsing the duplicate `x`. `IReadOnlySet` is included only when that interface exists. Interface-declared set sources use the default comparer because their declared contracts do not expose a comparer. +Unreleased (#30): set capacity is preallocated only when cheap count or length information and the exact accessible constructor are available. Set mappings use parameterless or comparer-only construction on targets such as `netstandard2.0` where capacity overloads are absent. Comparer corrections are described below. -The [dictionary comparer example](../samples/Mammoth.LiteMapper.Samples.Collections/DictionaryComparerExample.cs) preserves a concrete dictionary source's case-insensitive key comparer when widening `int` values to `long` or structurally mapping value objects. This applies to `Dictionary`, `IDictionary`, and `IReadOnlyDictionary` destinations. Comparer selection depends on the selected key conversion, independently of value conversion: custom key converters use the destination default comparer even when their input and output key types match. Converted-key collisions throw through normal `Add` semantics. Interface-declared dictionary sources use the default comparer because their declared contracts do not expose a comparer; LiteMapper does not discover it at runtime. +Unreleased (#30, #51): The [portable set example](../samples/Mammoth.LiteMapper.Samples.Collections/PortableSetExample.cs) maps arrays and lists to `HashSet` and `ISet`, collapsing duplicate converted values. It also verifies detached identity-mapped copies with preserved case-insensitive comparers. A selected custom element converter uses the destination default comparer even when its input and output types match: mapping `A`, `B`, and `C` to `x`, `X`, and `x` retains both `x` and `X` while collapsing the duplicate `x`. `IReadOnlySet` is included only when that interface exists. Interface-declared set sources use the default comparer because their declared contracts do not expose a comparer. + +Unreleased (#31): The [dictionary comparer example](../samples/Mammoth.LiteMapper.Samples.Collections/DictionaryComparerExample.cs) preserves a concrete dictionary source's case-insensitive key comparer when widening `int` values to `long` or structurally mapping value objects. This applies to `Dictionary`, `IDictionary`, and `IReadOnlyDictionary` destinations. Comparer selection depends on the selected key conversion, independently of value conversion: custom key converters use the destination default comparer even when their input and output key types match. Converted-key collisions throw through normal `Add` semantics. Interface-declared dictionary sources use the default comparer because their declared contracts do not expose a comparer; LiteMapper does not discover it at runtime. For array results from sources without a cheap, reliable count, mapping uses an O(n) temporary growing buffer and then creates the final array. It does not count by enumerating first, and each element is converted once. Counted sources allocate their final array directly. The same rule applies to interfaces whose concrete result is an array. @@ -869,7 +899,7 @@ public static partial class NullableStatusMapper The same name matching and null preservation apply to members and collection elements. A nullable source targeting a non-null enum follows `Error` or `Throw`; unknown numeric values still throw `ArgumentOutOfRangeException` under by-name mapping. Patch mappings with `IgnoreNullSourceMembers` preserve the existing enum value when the source is null. -The Basic sample includes a configured enum path with a nullable intermediate: +Unreleased (#27): The Basic sample includes a configured enum path with a nullable intermediate: ```csharp [LiteMapper(NullableMismatch = NullableMismatchPolicy.Throw)] @@ -882,7 +912,7 @@ public static partial class EnumPathMapper For this non-null enum target, `Ready = 1` maps by name to `Ready = 10`, a missing `Child` follows the Throw policy, and an unknown value throws `ArgumentOutOfRangeException` with `ParamName` equal to `Child.State` and the original enum in `ActualValue`. Root mappings retain their declared parameter name; direct members retain their source-member name. Exception metadata does not cause another getter evaluation. -For a nullable enum target, a missing reference intermediate preserves null even when the enum leaf is non-nullable. The Basic sample's `NullableEnumPathExample.cs` demonstrates creation, constructor binding, existing-target updates, and patch skipping for `Child.State`. By-name mapping converts `Ready = 1` to `Ready = 10` and retains the exception metadata above for unknown non-null values; by-value mapping preserves null and converts the underlying value according to the enum numeric policy. Explicit converters still take precedence and a nullable-input converter receives a missing path as null. +Unreleased (#48): For a nullable enum target, a missing reference intermediate preserves null even when the enum leaf is non-nullable. The Basic sample's `NullableEnumPathExample.cs` demonstrates creation, constructor binding, existing-target updates, and patch skipping for `Child.State`. By-name mapping converts `Ready = 1` to `Ready = 10` and retains the exception metadata above for unknown non-null values; by-value mapping preserves null and converts the underlying value according to the enum numeric policy. Explicit converters still take precedence and a nullable-input converter receives a missing path as null. ## Recursive mappings and cycle detection @@ -898,9 +928,9 @@ public static partial class CycleMapper `ReferenceHandling.ThrowOnCycle` enables generated cycle tracking for recursive type graphs. A detected cycle throws `LiteMapperCycleException`. Non-recursive mappings do not allocate cycle-tracker state. -Successful recursive traversal retains compact diagnostic path segments rather than allocating a full path string at every level. The complete `MemberPath` is formatted only when a cycle is detected; active diagnostic-path storage grows linearly with depth. This does not remove the call-stack limit of recursive mapping. The [Basic cycle example](../samples/Mammoth.LiteMapper.Samples.Basic/LazyCyclePathExample.cs) exercises finite chains, exact direct/indirect/collection paths, shared siblings, and later calls after exceptions. +Unreleased (#32): Successful recursive traversal retains compact diagnostic path segments rather than allocating a full path string at every level. The complete `MemberPath` is formatted only when a cycle is detected; active diagnostic-path storage grows linearly with depth. This does not remove the call-stack limit of recursive mapping. The [Basic cycle example](../samples/Mammoth.LiteMapper.Samples.Basic/LazyCyclePathExample.cs) exercises finite chains, exact direct/indirect/collection paths, shared siblings, and later calls after exceptions. -Acyclic object and collection wrappers can lead into a recursive graph. The same tracker is forwarded through those wrappers, while only recursive reference objects participate in cycle detection. The Basic sample maps `WrappedSource.Envelope.Nodes`; a self-cycle in a node reports `Envelope.Nodes.Next`, and a shared node in separate collection positions maps independently. +Unreleased (#25): Acyclic object and collection wrappers can lead into a recursive graph. The same tracker is forwarded through those wrappers, while only recursive reference objects participate in cycle detection. The Basic sample maps `WrappedSource.Envelope.Nodes`; a self-cycle in a node reports `Envelope.Nodes.Next`, and a shared node in separate collection positions maps independently. `LiteMapperCycleException` exposes `SourceType`, `DestinationType`, `MappingMethod`, and `MemberPath` for the detected active-path cycle. It does not expose `SourcePath` or `DestinationPath` properties. @@ -949,19 +979,24 @@ Windows and Linux are supported. A `netstandard2.0` library runs inside an appli The primary `Mammoth.LiteMapper` package is the normal install path. `Mammoth.LiteMapper.Generator` is the generator implementation package and is not the normal consumer install route. -The release workflow validates package API compatibility and clean-consumer execution, including trimming and Native AOT, before promoting artifacts. Each native validation and package push must succeed immediately; a failure stops the stage and reports the affected operation or package. See [the release checklist](RELEASE_CHECKLIST.md). - ## Analyzer options -The generator recognizes these build properties for diagnostics and development: +The generator recognizes these optional build properties for diagnostics and development. In the consuming project, set each value and export it with `CompilerVisibleProperty` so Roslyn passes it to the generator: -```text -LiteMapper_EmitDebugMetadata -LiteMapper_IncludeGeneratedSourceComments -LiteMapper_TreatInternalGeneratorErrorsAsExceptions +```xml + + true + true + false + + + + + + ``` -These options must not change mapping semantics. +Setting a property alone does not make it visible to the generator. These options do not change mapping semantics. Leave `LiteMapper_TreatInternalGeneratorErrorsAsExceptions` disabled except when investigating a generator failure. Unexpected mapper validation, planning, or rendering failures normally produce one sanitized `LITEMAPPER9001` at that mapper while unrelated mappers continue. `LiteMapper_TreatInternalGeneratorErrorsAsExceptions=true` exposes the original exception through the compiler's generator-failure reporting for development and tests. Exception details can contain local paths; remove sensitive details before sharing a report. diff --git a/skills/mammoth-litemapper/SKILL.md b/skills/mammoth-litemapper/SKILL.md index dc19443..ca128d0 100644 --- a/skills/mammoth-litemapper/SKILL.md +++ b/skills/mammoth-litemapper/SKILL.md @@ -5,18 +5,20 @@ description: Use Mammoth.LiteMapper in C# applications to declare compile-time o # Mammoth.LiteMapper -Generate ordinary C# mapping calls from bodyless partial methods. This skill targets consumer usage of Mammoth.LiteMapper 3.0.0, not development of the generator. +Generate ordinary C# mapping calls from bodyless partial methods. This skill covers consumer usage of published `3.0.0` and explicitly marked unreleased corrections, not development of the generator. ## Start with the consumer Inspect the consumer's package version, target framework, language version, nullable context, source/destination models, and existing mapper conventions. Preserve the application's intended null, update, and conversion semantics. Ask when those semantics are unclear. -Use the primary `Mammoth.LiteMapper` NuGet package. One package reference supplies the abstractions and generator analyzer; do not add Roslyn runtime dependencies or install the generator package as the normal consumer route. Follow existing central package management when present. This skill documents release `3.0.0`; verify availability in the consumer's configured feed before changing versions. +Use the primary `Mammoth.LiteMapper` NuGet package. One package reference supplies the abstractions and generator analyzer; do not add Roslyn runtime dependencies or install the generator package as the normal consumer route. Follow existing central package management when present. Use `3.0.0` for the stable install shown below and verify availability in the consumer's configured feed before changing versions. Unreleased guidance is not a package-version recommendation; installing this skill does not upgrade the generator. ```xml ``` +Before relying on an advanced mapping, read [version boundaries](references/mapping-rules.md#version-boundaries). In particular, the stricter `IgnoreCase` ambiguity check, corrected dictionary/set comparer selection, and nullable enum-path fixes are unreleased. Do not promise them for `3.0.0` or invent a published next version. + The documented support matrix is `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0`, with C# 9+ and a Roslyn 4.8.0+ compiler host. Shipping assemblies target `netstandard2.0`. Newer model syntax such as `required` still needs the corresponding compiler capabilities. ## Declare and call a mapping @@ -49,7 +51,7 @@ Call `new InstanceMapper().Map(new InstanceSource { Id = 42 })`. Static mappers - New-object methods take one by-value source parameter and return the destination. Use explicit `public`, `internal`, or `private` accessibility. - Reference updates take source and destination and return `void` or the destination. Value-type updates require `ref` on the destination. - Instance mappers can use their own dependencies and converters. External `[UseMapper(typeof(...))]` containers must be static; closed constructed generic containers are allowed, while unbound generic containers and generic methods are not. No library DI registration extension exists. -- Assembly-level `LiteMapperDefaults` values and `UseMapper` registrations are compile-time inputs. Changing one replans affected generated mappings and diagnostics during an incremental build; unrelated output remains cached where the host preserves candidate identity. Never describe `UseMapper` as runtime registration. +- Assembly-level `LiteMapperDefaults` values and `UseMapper` registrations are compile-time configuration. Rebuild after changing them; `UseMapper` does not register anything at runtime. ## Choose the relevant mapping rules @@ -69,6 +71,6 @@ Do not invent runtime `IMapper`, `Map(object)`, registration/scann Build the affected consumer project and inspect the actual `LITEMAPPER` diagnostic before changing configuration. Test the behavior relevant to the request: null preservation or throwing, destination identity, collection replacement/copying, converter results, and cycles where applicable. Use the consumer's existing test conventions. Do not suppress hard semantic errors or insert `default!` to fabricate a valid mapping. -Source-generated implementations are private build artifacts apart from the methods the consumer declared. Nested and collection helper names are private, allocated within each generated mapper, and deterministic. Handwritten members and other generated helper signatures reserve names; identical closed source/destination identities with equivalent effective options, nullable shape, and execution context reuse one name, while distinct semantic identities are disambiguated even if preferred shape-plus-hash names collide. Allocation is independent of syntax-tree or equivalent compilation order. Do not edit generated files or assume extra public collection overloads exist, and do not depend on helper names or signatures. Do not infer deep copies of identical non-collection reference types; absent an explicit mapping, the same reference is assigned. +Do not edit generated files, depend on private helper names or signatures, or assume extra public collection overloads exist. Call the mapping methods declared by the consumer. Identical non-collection reference types reuse the source reference unless an explicit mapping is selected; mutable collections are copied. -For version-specific behavior, consult the matching upstream revision. For 3.0.0, `SPECIFICATION.md` is the product contract, `docs/USAGE.md` is the consumer guide, and the public API, compiling samples, and tests establish demonstrated usage. Report discrepancies rather than silently treating documentation or implementation drift as a new contract. +Use the [pinned upstream sources](references/mapping-rules.md#upstream-sources) for the installed release or the identified unreleased revision. Build examples against that package; report a mismatch rather than treating the newer documentation as proof of released behavior. diff --git a/skills/mammoth-litemapper/references/mapping-rules.md b/skills/mammoth-litemapper/references/mapping-rules.md index 7dbe4fc..1474257 100644 --- a/skills/mammoth-litemapper/references/mapping-rules.md +++ b/skills/mammoth-litemapper/references/mapping-rules.md @@ -1,9 +1,22 @@ # Mapping rules -Consumer guidance for Mammoth.LiteMapper 3.0.0. `SPECIFICATION.md` is the normative product contract, `docs/USAGE.md` is the consumer guide, and the matching public API, samples, and tests demonstrate release behavior. Consult the revision matching the installed package when behavior differs. +Consumer guidance for published `3.0.0`, with unreleased corrections explicitly identified below. Use the [pinned upstream sources](#upstream-sources) for the installed package. The specification defines the contract; samples and tests demonstrate what a particular revision implements. Nested mapper classes may live in non-generic partial records, record structs, or interfaces; every containing type must be partial and the mapper itself must remain a class. For example, a partial record Container can contain a static partial Mapper class. Attribute enum constants are validated by value: `(NameMatching)3` means IgnoreCase and maps source value to target Value; undefined999 is rejected, including in assembly defaults. +## Version boundaries + +`3.0.0` is the published stable install used by this skill. Unreleased notes refer to development revision [f0d4bee](https://github.com/PrimordialCode/Mammoth.LiteMapper/commit/f0d4beedc1c0f0105c0e2a5c37c283969ac23f96), not an available next-version package. Preserve the consumer's package choice and do not infer that a skill update changes its generator. + +Before applying unreleased guidance, check these affected shapes: + +- `IgnoreCase`: `3.0.0` can choose an exact member from multiple case-insensitive matches; the correction rejects the ambiguity with `LITEMAPPER1004` (#28). +- Comparers: `3.0.0` can preserve a source set comparer after same-type custom conversion, collapsing mapped values that should be distinct. Dictionary value conversion can discard a key comparer, and same-type custom key conversion can retain it incorrectly. The corrections change membership, lookup, counts, and collision behavior (#31, #51). +- Set construction on `netstandard2.0`, nullable structural collection elements, and guarded nullable enum source paths need the corresponding unreleased fixes (#26, #27, #30, #48). The published enum generator can fail to compile a guarded path. +- Different entry policies for shared models, instance converters inside automatic helpers, omitted-source root converters, attributed partial mapper parts, cycle wrappers, and captured nullable patch-updater arguments have additional unreleased corrections (#21-25, #29, #47). + +For an affected `3.0.0` consumer, reproduce the case against that package and use an explicit handwritten mapping when needed. Consult the [changes since 3.0.0](https://github.com/PrimordialCode/Mammoth.LiteMapper/compare/3.0.0...f0d4beedc1c0f0105c0e2a5c37c283969ac23f96) before recommending an upgrade; no next release number is assigned here. + ## Members and construction Populate read-only fields/properties through a selected constructor. For example, Target(int value) can initialize readonly Value from source Value3. An ordinary unbound read-only value field follows UnmappedTargetMembers and retains its value; do not emit an illegal initializer assignment. Required/non-nullable obligations remain enforced. @@ -21,7 +34,7 @@ private static string MapDisplayName(Customer source) => source.FirstName + " " + source.LastName; ``` -This is the usage guide's explicit-member pattern; adapt the model names and ensure every destination member is satisfiable. Source paths contain readable public fields/properties only, with no calls, indexing, or expressions. Targets are direct member names, never dotted paths. Omitting `Source` with explicit `Use` passes the root source to the converter. +This is the usage guide's explicit-member pattern; adapt the model names and ensure every destination member is satisfiable. Source paths contain readable public fields/properties only, with no calls, indexing, or expressions. Targets are direct member names, never dotted paths. Omitting `Source` with explicit `Use` selects the root source; the unreleased correction (#23) ensures automatic member matching cannot override that explicit selection. `[IgnoreTarget(nameof(Target.Member))]` excludes a target assignment. `[IgnoreSource(nameof(Source.Member))]` controls unused-source reporting, not target assignment. `[UseTargetDefault(nameof(Target.Member))]` deliberately preserves a real initializer or optional constructor default; it cannot invent a default or satisfy an unsatisfied required contract. @@ -35,11 +48,11 @@ Constructor arguments accept explicit source configuration and converters, inclu Converters are synchronous, non-generic methods with one source argument and a compatible return. Use explicit `MapProperty.Use`, `[MappingConverter]`, or `Map{TargetMember}`. Do not assume an arbitrary compatible helper will be discovered. Register external static containers with `[UseMapper]`; use `ConverterType` for explicit external selection outside local/registered resolution. Use `[DefaultMapping]` to disambiguate a source/destination pair within the local or registered external mapping stage being considered. Converter exceptions propagate unchanged. -Assembly-level defaults and `[UseMapper]` registrations are compile-time inputs to incremental planning. A changed value or registration replans affected generated mappings and diagnostics, while unrelated output may remain cached when the host preserves candidate identity. `[UseMapper]` does not create a runtime registry. +Assembly-level defaults and `[UseMapper]` registrations are compile-time configuration. Rebuild after changing them; `[UseMapper]` does not create a runtime registry. An explicit `Use = nameof(Parse)` can select an accessible inherited handwritten converter, following normal C# member lookup. For example, a protected base `int Parse(string value) => int.Parse(value) + 1` maps `"12"` to `13`. Instance converters require an instance mapping method; private base methods are inaccessible. Mapper configuration is not implicitly inherited. -Automatic nested and collection helpers preserve the calling mapper instance for selected instance converters and mappings, including transitive helper calls. Instance-free helpers remain static. Static mapping methods inside instance mapper classes can use only static converters and mappings. The Basic sample demonstrates separate constructor-provided prefixes for nested objects and collection elements; user-owned mutable state still requires consumer synchronization. +Unreleased (#22): automatic nested and collection mappings preserve the mapper instance for selected instance converters and mappings. Static entry methods use only static converters and mappings. The development Basic sample demonstrates separate constructor-provided prefixes for nested objects and collection elements. User-owned mutable state still requires consumer synchronization. Register static containers, including closed constructed generic types, exposing an accessible supported synchronous converter or mapping, including two-parameter updates. A container with `int Parse(string value)` is usable even for unrelated mapping pairs; one with only `void Ping()` reports `LITEMAPPER0010`. Unbound generic containers and generic methods are unsupported. Missing types also report0010, while existing non-static containers report0011. Normal C# accessibility permits private converters from a nested mapper; assembly registration does not make inaccessible methods callable. @@ -51,7 +64,7 @@ A nullable reference or value converter result targeting a non-null member repor Generated eligible partial mappings are discovered automatically. Handwritten local methods require one of those explicit mechanisms or `[DefaultMapping]`; names such as `ToDto` or a matching scalar/structural signature alone do not make a helper eligible. Explicit registration opts in compatible methods from a static external container. Mark converters used for dictionary keys/values or collection elements with `[MappingConverter]` when they should participate automatically; marked converters outrank identity conversion. -Generated nested and collection helpers are private mapper implementation details. Their names are allocated per generated mapper, reserve handwritten members and other generated helper signatures, reuse one name for an identical closed source/destination identity only when effective options, relevant nullable shape, and execution context are equivalent, and deterministically disambiguate distinct semantic identities when preferred shape-plus-hash names collide. Allocation does not depend on syntax-tree or equivalent compilation order. Consumer code must not depend on helper names or signatures. +Generated nested and collection helpers are private; do not depend on their names or signatures. Unreleased correction (#21): helpers consistently retain the calling entry's options and nullable shape. Recheck entries that use the same models with different policies when upgrading. Resolve explicit `MapProperty.Use` first, then an explicit source path (completing immediately if directly assignable), local marked converters, the local member convention, local mappings, registered external converters, and registered external mappings, before language/enum/collection/structural conversion. A unique default selects among mapping methods within its stage; it never skips an earlier stage. @@ -83,7 +96,7 @@ Options resolve method `[MappingOptions]`, mapper `[LiteMapper]`, assembly `[Lit For an opt-in strict source-completeness boundary, set `UnmappedSourceMembers` to `Error` at mapper, method, or assembly scope. This reports unused readable source members as configurable `LITEMAPPER1003` diagnostics naming the member; use `Warning` or `Info` to stage adoption and `IgnoreSource` for intentional exclusions. The default remains `Ignore` because source models can contain fields that are not part of a given destination contract. Source completeness does not weaken target completeness or mandatory target errors. -Name matching uses ordinal comparisons. `Exact` accepts only an exact name; `ExactThenIgnoreCase` prefers exact and otherwise requires a unique case-insensitive match. `IgnoreCase` requires a unique case-insensitive match without exact priority: source `Value` plus `value` targeting `Value` reports non-configurable `LITEMAPPER1004`, including in updates and nested structural mapping. Explicit `MapProperty.Source` can disambiguate. Constructor parameters follow the same policy; hidden members are resolved before matching. +Name matching uses ordinal comparisons. `Exact` accepts only an exact name; `ExactThenIgnoreCase` prefers exact and otherwise requires a unique case-insensitive match. Unreleased correction (#28): `IgnoreCase` rejects source `Value` plus `value` targeting `Value` with non-configurable `LITEMAPPER1004`, including updates and nested structural mappings. Published `3.0.0` incorrectly selects the exact member in this case. Explicit `MapProperty.Source` can select the intended member. Constructor matching already follows the unique-ignore-case contract; hidden members are resolved before matching. ## Nulls and updates @@ -100,23 +113,35 @@ Name matching uses ordinal comparisons. `Exact` accepts only an exact name; `Exa - Writable nested members and collections are replaced by default. Do not infer merging or get-only collection population. Nested in-place mutation needs an explicit compatible update mapping and confirmation that the installed version supports the requested shape. - A non-null get-only child can use a declared `void ApplyChild(ChildSource source, ChildTarget target)`: source Value3 updates the existing child to3 without replacing it. Explicit `Use` with a configured source selects a handwritten updater; a configured path such as `Other.Item` supplies that nested source. Class-level external registration precedes assembly-level registration: an updater adding20 wins over an assembly updater adding10, producing23 for source3. Equally eligible updaters in the same registration scope require disambiguation or report `LITEMAPPER3001`. Verify nullable and recursive combinations against the installed revision before relying on them. +When explicitly selecting an overloaded nested updater with `MapProperty.Use`, identity source compatibility wins. For example, for a `ChildSource` member, `Chosen(ChildSource source, ChildTarget target)` wins over `Chosen(object source, ChildTarget target)`. If the selected overload adds `10`, source value `3` becomes target value `13`. + +For a writable non-null child, select the updater explicitly with `[MapProperty(Source = nameof(Source.Child), Target = nameof(Target.Child), Use = nameof(ApplyChild))]`. With `void ApplyChild(ChildSource source, ChildTarget target)`, source value `3` updates the original child to `3` and preserves its identity. Without explicit updater selection, writable children use replacement by default. + +Omitting `Source` in `[MapProperty(Target = nameof(Target.Child), Use = nameof(ApplyChild))]` passes the root source to `void ApplyChild(Source source, ChildTarget target)`. No source member named `Child` is required: the updater can copy root `Value == 3` into `target.Child.Value`. + +Unreleased (#29, #47): patch guards and nested updater calls share one captured source value, and nullable value captures preserve the selected overload for automatic direct members and configured direct or dotted paths. Published `3.0.0` can read a direct getter again or call a competing non-nullable overload for a configured nullable value. + +Nested updater calls obey patch and nullability rules. With `IgnoreNullSourceMembers`, null `source.Child` skips the updater and preserves the destination. Under the default mismatch policy, nullable child to a non-null updater parameter reports `LITEMAPPER2001`; `Throw` identifies the member path. A returning updater may create and return a nullable writable child, and the parent assigns it. A nullable get-only child cannot store a returned replacement and reports `LITEMAPPER5005`. + ## Collections, enums, and cycles -Set capacity is used only when the consumer compilation exposes the exact accessible constructor. Arrays and lists can map to `HashSet` or `ISet` on `netstandard2.0` through a parameterless/comparer-only fallback; `IReadOnlySet` requires that interface to exist. Capacity selection does not change duplicate collapsing, compatible-comparer preservation, or independent collection identity. +Unreleased (#30): arrays and lists can map to `HashSet` or `ISet` on `netstandard2.0` without requiring unavailable capacity constructors. Published `3.0.0` can fail compilation for this shape. `IReadOnlySet` still requires that interface to exist; constructor selection does not change the mapped contents. Enum numeric policy applies to declared values and unmatched-name ByValue fallback: Unchecked300 to a byte-backed enum yields44; Checked retains overflow validation. Missing composite flag names may map through their atoms (target Read4 OR Write8 gives12), including signed high-bit source flags; unknown source bits throw. A matching composite name must equal the mapped atomic OR: target Both12 is valid, Both16 reports LITEMAPPER7002 and omits the mapping. Every ordinary source alias must resolve consistently: Known/Alias1 may map to Known/Alias9. Declare public top-level collection mappings explicitly, for example `public static partial List MapCustomers(Customer[] source);` inside a static mapper. Supported shapes include one-dimensional/jagged arrays, common generic sequence/list interfaces, `List`, sets, and dictionaries; `IReadOnlySet` requires the symbol in the compilation. -Mutable collections are copied; elements are not necessarily deep-cloned. Arbitrary enumerable sources are traversed once. Null elements are not silently removed. Dictionary keys and values map separately; duplicate converted keys throw through normal `Add` semantics. Compatible identity element mappings can preserve set comparers. A selected custom element converter uses the destination default comparer even for identical element types; values distinct under that comparer survive and genuine duplicates may collapse. Concrete Dictionary sources preserve compatible key comparers independently of value conversion, including widening or structural values. A selected custom key converter uses the destination default comparer even for identical key types; interface-declared sources do not expose a comparer for preservation. Do not generate custom/immutable collection construction, queues, stacks, rectangular arrays, or async enumerables. +Mutable collections are copied; elements are not necessarily deep-cloned. Arbitrary enumerable sources are traversed once. Null elements must not be silently removed; nullable structural element preservation needs the unreleased correction (#26). Dictionary keys and values map separately; duplicate converted keys throw through normal `Add` semantics. Do not generate custom/immutable collection construction, queues, stacks, rectangular arrays, or async enumerables. + +Unreleased comparer corrections (#31, #51): concrete set sources preserve compatible comparers only for identity element mappings. A selected custom element converter uses the destination default comparer even for identical element types, so `x` and `X` both survive under default string equality while repeated `x` collapses. Concrete dictionary sources preserve compatible key comparers independently of value conversion; selected custom key converters use the destination default comparer even for identical key types. Interface-declared sources do not expose a comparer for preservation. Recheck membership, counts, lookups, and key collisions when upgrading from `3.0.0`. Interface destinations use `T[]` for `IEnumerable`, `IReadOnlyCollection`, and `IReadOnlyList`; `List` for `ICollection` and `IList`; `HashSet` for set interfaces; and `Dictionary` for dictionary interfaces. The same result types apply to nested members and `NullCollections.Empty`. In 3.0.0, regression coverage verifies array results, mutable-list results, independent copies, one-pass enumeration, and nested/null-to-empty mappings. -An unknown-count enumerable-to-array mapping may allocate a temporary growing buffer plus its final array under the approved section 23.3 exception. Temporary storage/allocation is O(n); enumeration and element conversion happen once. With a cheap, reliable count, allocate the final array directly. This also applies to array-backed interfaces. Do not promise destination-only allocations for unknown-count arrays; use the repository's `CollectionAllocationBenchmarks` to compare measured allocation against its corresponding manual baseline. +An unknown-count enumerable-to-array mapping may allocate a temporary growing buffer plus its final array under the approved section 23.3 exception. Temporary storage/allocation is O(n); enumeration and element conversion happen once. With a cheap, reliable count, allocate the final array directly. This also applies to array-backed interfaces. Do not promise destination-only allocations for unknown-count arrays. Enum defaults are `EnumMappingStrategy.ByName`, `UnmatchedEnumValuePolicy.Error`, and `EnumNumericConversion.Checked`. Names match exactly. `Throw` permits unmatched names with runtime `ArgumentOutOfRangeException`; by-value conversion uses the independent enum numeric policy. Domain-specific enum semantics belong in a converter. -In development revisions containing the nullable enum-path repair, a missing reference intermediate in `Child.State` maps to null when the destination enum is nullable, even when `State` is non-nullable. By-name unknown values still throw with the configured path and original value; by-value preserves null before numeric conversion. A nullable-input explicit converter still receives null. Verify the installed revision before relying on this shape; the published 3.0.0 generator may fail to compile the guarded path. +Unreleased (#27, #48): a missing reference intermediate in `Child.State` maps to null when the destination enum is nullable, even when `State` is non-nullable. By-name unknown values throw with the configured path and original value; by-value preserves null before numeric conversion. A nullable-input explicit converter still receives null. Published `3.0.0` can fail to compile the guarded path; use an explicit handwritten mapping for that version. `ReferenceHandling.ThrowOnCycle` detects active-path reference cycles in recursive generated mappings and throws `LiteMapperCycleException`. Shared references on separate branches are not cycles and identity is not preserved. Without tracking, recursive runtime cycles can overflow the stack. The exception exposes `SourceType`, `DestinationType`, `MappingMethod`, and `MemberPath`. @@ -140,24 +165,22 @@ For configuration troubleshooting, use `1006` invalid source path, `1007` dotted Unexpected planning failures normally report one sanitized `LITEMAPPER9001` per failing mapper. The documented `LiteMapper_TreatInternalGeneratorErrorsAsExceptions` development option exposes full exception details through compiler generator-failure reporting; redact local paths and proprietary details before sharing them. +To enable a diagnostic build property, also export it through a `CompilerVisibleProperty` item. For example, pair `true` in `PropertyGroup` with `` in `ItemGroup`. Apply the same pattern to `LiteMapper_IncludeGeneratedSourceComments` or `LiteMapper_TreatInternalGeneratorErrorsAsExceptions`; setting the property alone does not pass it to Roslyn. + ## Validation boundaries - Representative package-consumer checks cover `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0` with C# 9, C# 14, and `latest`. Compiler-host checks cover Roslyn 4.8, 4.14, and 5.9. These representative cases do not prove every feature on every compiler host. - Native AOT is validated separately and requires platform linker prerequisites. For example, Linux publishing requires Clang and zlib development files. -Upstream sources (select the exact tag or commit matching the installed package): +## Upstream sources -- [Specification](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/main/SPECIFICATION.md): sole product contract, especially §§5-18 and 20. -- [Usage guide](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/main/docs/USAGE.md): consumer patterns. -- [README](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/main/README.md): package quickstart. -- [Basic sample](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/main/samples/Mammoth.LiteMapper.Samples.Basic/Program.cs): static/instance, nested collection, and cycle examples. -- [Collections sample](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/main/samples/Mammoth.LiteMapper.Samples.Collections/Program.cs): collection mapping. -- [ASP.NET Core sample](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/main/samples/Mammoth.LiteMapper.Samples.AspNetCore/Program.cs): direct mapper call from an endpoint. +Released `3.0.0` sources (these older documents do not include the unreleased corrections above): -When explicitly selecting an overloaded nested updater with `MapProperty.Use`, identity source compatibility wins. For example, for a `ChildSource` member, `Chosen(ChildSource source, ChildTarget target)` wins over `Chosen(object source, ChildTarget target)`. If the selected overload adds `10`, source value `3` becomes target value `13`. - -For a writable non-null child, select the updater explicitly with `[MapProperty(Source = nameof(Source.Child), Target = nameof(Target.Child), Use = nameof(ApplyChild))]`. With `void ApplyChild(ChildSource source, ChildTarget target)`, source value `3` updates the original child to `3` and preserves its identity. Without explicit updater selection, writable children use replacement by default. +- [Specification](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/SPECIFICATION.md): sole product contract, especially §§5-18 and 20. +- [Usage guide](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/docs/USAGE.md): consumer patterns. +- [README](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/README.md): package quickstart. +- [Basic sample](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/samples/Mammoth.LiteMapper.Samples.Basic/Program.cs): static/instance, nested collection, and cycle examples. +- [Collections sample](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/samples/Mammoth.LiteMapper.Samples.Collections/Program.cs): collection mapping. +- [ASP.NET Core sample](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/samples/Mammoth.LiteMapper.Samples.AspNetCore/Program.cs): direct mapper call from an endpoint. -Omitting `Source` in `[MapProperty(Target = nameof(Target.Child), Use = nameof(ApplyChild))]` passes the root source to `void ApplyChild(Source source, ChildTarget target)`. No source member named `Child` is required: the updater can copy root `Value == 3` into `target.Child.Value`. - -Patch guards and nested updater calls share the same captured source value, so direct nullable getters are read once and null skips the updater. Nullable value captures preserve the selected updater overload for both automatic direct members and configured direct or dotted source paths. Nested updater calls obey patch and nullability rules. With `IgnoreNullSourceMembers`, null `source.Child` skips the updater and preserves the destination. Under the default mismatch policy, nullable child to a non-null updater parameter reports `LITEMAPPER2001`; `Throw` identifies the member path. A returning updater may create and return a nullable writable child, and the parent assigns it. A nullable get-only child cannot store a returned replacement and reports `LITEMAPPER5005`. +For the unreleased corrections, use the [f0d4bee usage guide](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/f0d4beedc1c0f0105c0e2a5c37c283969ac23f96/docs/USAGE.md), [specification](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/f0d4beedc1c0f0105c0e2a5c37c283969ac23f96/SPECIFICATION.md), and [samples](https://github.com/PrimordialCode/Mammoth.LiteMapper/tree/f0d4beedc1c0f0105c0e2a5c37c283969ac23f96/samples). These links identify a source snapshot, not a published package. From c00821a6bb6504fcc1d621caf335f2aa9818c137 Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Mon, 12 Oct 2026 16:06:46 +0200 Subject: [PATCH 2/2] docs: make consumer skill self-contained and highlight breaking changes Describe current release behavior directly, keep older versions in migration guidance, and bundle all skill documentation. Add prominent conditional compatibility warnings to the changelog and record validation boundaries. Co-authored-by: Codex --- CHANGELOG.md | 18 +++-- IMPLEMENTATION_PLAN.md | 11 +-- STATUS.md | 10 +++ skills/mammoth-litemapper/SKILL.md | 18 ++--- .../references/mapping-rules.md | 73 +++++++------------ 5 files changed, 65 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d74bdaa..958ef82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ The following changes are not included in the published `3.0.0` package. No next release version is assigned here. +### Breaking Changes + +These are conditional source/runtime compatibility changes from `3.0.0`, even though public package API compatibility checks pass. They correct unintended behavior and affect consumers using the described shapes; they are not universal API removals. + +- **Ambiguous name matching (#28):** `IgnoreCase` mappings that previously selected an exact member from multiple case-insensitive matches now fail with `LITEMAPPER1004`. Select the intended source with `MapProperty.Source`, or use `Exact`/`ExactThenIgnoreCase` when exact-name priority is intended. +- **Collection equality (#31, #51):** dictionary value-only conversion now preserves a compatible source key comparer; custom key or set-element conversion uses the destination default comparer. Lookups, duplicate-key exceptions, set membership, and element counts can change. Recheck equality-sensitive mappings and collision expectations after upgrading. +- **Per-entry policies (#21):** mappings sharing model pairs now consistently honor each entry's numeric, enum, null, and cycle policies. Previously masked nullable/null-collection errors can surface as `LITEMAPPER2001` or `LITEMAPPER2002`; checked/unchecked, by-name/by-value, and preserve/empty results can differ. Review each entry's intended policies and satisfy reported nullability obligations instead of relying on another entry's helper behavior. +- **Explicit root-source converters (#23):** omitted-`Source` configuration invokes the root converter even when no matching source member exists and unmapped targets are ignored; a former default value can become the converter result. Patch calls no longer depend on an unrelated same-name nullable member. Recheck converter outputs and patch null behavior; select a source member explicitly when member-based input is intended. +- **Patch getter and overload behavior (#29, #47):** guarded source getters are consumed once, and captured nullable values retain the selected nullable updater overload instead of binding a competing non-nullable overload. Side effects, selected updater results, and exceptions from a second getter read can change. Recheck stateful getters and overloaded updaters; do not rely on a second read or the previously incorrect overload. +- **Null structural elements (#26):** built-in structural collection mapping now preserves null for nullable element targets. For nonnullable element targets under `Throw`, it raises `InvalidOperationException` before constructing the target object or reading source members, rather than dereferencing null in the helper. Higher-precedence nullable converters still receive null. Recheck null handling, side effects, and exception expectations. +- **Enum exception metadata (#27):** nullable-root by-name enum failures now identify the declared input parameter instead of a generated capture name. Update assertions or error handling that inspect `ParamName` or related exception text. + ### Fixed - Issue #51: set mappings use the destination default comparer after custom element conversion, including same-type converters, preventing incorrect collapse of distinct mapped values. Identity-mapped sets retain compatible source comparers. [#51](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/51) @@ -24,12 +36,6 @@ The following changes are not included in the published `3.0.0` package. No next - Issue #20: release validation and each NuGet package push now stop immediately on a nonzero native exit, reporting the failed operation or package before later successes can conceal it. [#20](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/20) - Issue #19: tag-selected manual workflow dry runs no longer enter the NuGet publish job. Automatic tag publishing requires a push event; manual branch publishing retains its explicit publish and dry-run gates. [#19](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/19) -### Upgrade considerations - -- #28: ambiguous `IgnoreCase` mappings that compiled in `3.0.0` can now fail with `LITEMAPPER1004`. Select the intended source with `MapProperty.Source`, or use `Exact`/`ExactThenIgnoreCase` when exact-name priority is intentional. -- #31: corrected dictionary comparer selection can change key lookup and collision behavior. Value-only conversion now preserves a compatible source key comparer; custom key conversion uses the destination default comparer. Verify lookups and duplicate-key exceptions. -- #51: corrected set comparer selection can change membership and element counts after custom conversion. Values distinct under the destination default comparer are retained; duplicates under that comparer still collapse. Verify membership and collision expectations before upgrading. - ## 3.0.0 ### Breaking Changes diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index bd2c449..9bbe75e 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -1,11 +1,12 @@ # Mammoth.LiteMapper Implementation Plan -## Consumer documentation release boundaries (2026-10-12) +## Consumer documentation release guidance (2026-10-12) -- Scope: correct README/usage/consumer-skill installation and version boundaries, analyzer-property exports, and unreleased changelog placement. Preserve published `3.0.0` installation; no product source, public API, specification semantics, or package version changes. -- Plan: distinguish tagged `3.0.0` from the unreleased f0d4bee snapshot, pin supporting source links, document central package management separately, and explain upgrade effects for ambiguity and comparer fixes. Keep consumer guidance focused on recipes, configuration, diagnostics, and compatibility. -- Validation: run stable-package quickstart and analyzer-option probes, central-package restore/build, skill format/discovery/install checks where available, source-backed usage validation, and a scoped diff/line-ending review. Final PR-SHA release validation is tracked separately. -- Operations: isolated cloud worktree `codex-consumer-release-docs`; AgentStack CLI is absent and the authorized connector fallback omits issue comments/claim writes. Stop at draft PR/human review; no merge, tag, package publication, or workflow dispatch. +- Scope: correct README/usage installation, analyzer-property exports, consumer-skill guidance, and prominent changelog compatibility warnings. No product source, public API, specification semantics, or package version changes. +- Review correction: describe the current library behavior directly as the release contract in the consumer skill and its reference. Mention previous versions only for breaking-change migration, not ordinary recipes. Use factual package-install commands without claiming an unpublished package is available. Keep the distributable skill self-contained: all document references and links stay within its own folder, with essential consumer guidance bundled locally. +- Validation: preserve the completed published-package quickstart/analyzer/central-management probes, repeat changed-skill format/discovery/install/link checks, including isolated installation and every bundled document target and source-backed usage validation, then verify exact-head Windows/Linux and Roslyn CI. Bind artifact/release evidence to its actual SHA and version rather than carrying it forward as final-head acceptance. +- Release readiness: separately track final-version artifact validation, nonpublishing workflow rehearsal, and administrator-controlled release safeguards. Performance evidence is separate and does not authorize changing its policy. +- Operations: isolated cloud worktree `codex-consumer-release-docs`; AgentStack CLI is absent and the authorized connector fallback omits issue comments/claim writes. Stop at draft PR/human review. No merge, release, tag, package publication, protection changes, or duplicate login request. ## Issue #51: comparer selection after set element conversion (2026-10-12) diff --git a/STATUS.md b/STATUS.md index 85fcde7..f1f0d22 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,5 +1,15 @@ # Mammoth.LiteMapper Status +## Consumer skill review correction (2026-10-12) + +- User-directed correction: the consumer skill and mapping reference describe the current library release contract directly. Previous versions appear only in breaking-change migration; normal recipes no longer frame supported behavior as upcoming or unreleased. The package command uses the consumer's configured feed and does not invent an available version. All skill document links and references stay inside its distributable folder; essential guidance is bundled instead of redirecting to repository documentation. This supersedes the initial skill framing in the checkpoint below. +- Scope: skill/reference prose, prominent conditional breaking-change notes in the changelog, and control-document updates only. README/usage installation and analyzer-export corrections remain intact; no library source, test, project/dependency, workflow, specification, or package-version change. +- Correction validation: skill format, cached-CLI discovery, isolated installation, installed-file byte equality, and both bundled document links pass. No external document links or repository/specification/sample redirects remain. The exact `dotnet add package Mammoth.LiteMapper` command resolves 3.0.0 from the existing configured feed; the skill quickstart builds with zero warnings/errors and returns Id=42. Focused usage validation passes 1/1 with zero skips. Final wording-only evidence refinements repeat the format/install/link/content checks; source/sample code is unchanged. +- Prior-head evidence: CI 38234941066 passed at bd7b366, with 1,315 tests each on Windows/Linux and 1,034 in each of six Roslyn jobs, zero failures/skips. Five published-package documentation probes and the earlier skill checks passed. Changed-skill revalidation and the final correction-head CI are recorded in PR #53 rather than represented as evidence for an untested SHA. +- Non-performance release gaps: no additional product-code defect was established. Provisional 3.1.0 artifacts are stamped bd7b366, not this correction; local final-package trimming/AOT failed at MSBuild task-host creation, including the supported trimming retry. Ordinary CI skipped release pack/validation and artifact-handoff jobs. A full nonpublishing rehearsal and selected final-version/ref artifacts remain necessary. +- Safeguards: read-only HTTP 200 checks at 13:52 UTC still found no repository/inherited rulesets and no production required-reviewer rules. The sole secure GitHub login request remains unresolved; no duplicate request or dispatch occurred. Performance evidence is tracked separately; this correction does not rerun benchmarks or declare overall release readiness. +- Boundary: retain draft PR/human review. No merge, tag, release, publication, credential handling, or protection-setting change. + ## Consumer documentation release-boundary checkpoint (2026-10-12) - Scope: documentation only, based on develop f0d4bee in isolated `codex-consumer-release-docs`. Stable `3.0.0` installation remains available; unreleased behavior and upgrade effects are explicit, upstream source links are pinned, central package management is separate, and analyzer options include compiler-visible exports. Changelog #29 is under Unreleased, #47 is included, and #28/#31/#51 have upgrade considerations. No package version, source, public API, or specification semantics changed. diff --git a/skills/mammoth-litemapper/SKILL.md b/skills/mammoth-litemapper/SKILL.md index ca128d0..546f9a7 100644 --- a/skills/mammoth-litemapper/SKILL.md +++ b/skills/mammoth-litemapper/SKILL.md @@ -5,25 +5,25 @@ description: Use Mammoth.LiteMapper in C# applications to declare compile-time o # Mammoth.LiteMapper -Generate ordinary C# mapping calls from bodyless partial methods. This skill covers consumer usage of published `3.0.0` and explicitly marked unreleased corrections, not development of the generator. +Generate ordinary C# mapping calls from bodyless partial methods. This skill describes the current Mammoth.LiteMapper release contract for consumer applications. ## Start with the consumer Inspect the consumer's package version, target framework, language version, nullable context, source/destination models, and existing mapper conventions. Preserve the application's intended null, update, and conversion semantics. Ask when those semantics are unclear. -Use the primary `Mammoth.LiteMapper` NuGet package. One package reference supplies the abstractions and generator analyzer; do not add Roslyn runtime dependencies or install the generator package as the normal consumer route. Follow existing central package management when present. Use `3.0.0` for the stable install shown below and verify availability in the consumer's configured feed before changing versions. Unreleased guidance is not a package-version recommendation; installing this skill does not upgrade the generator. +Use the primary `Mammoth.LiteMapper` NuGet package. One package reference supplies the abstractions and generator analyzer; do not add Roslyn runtime dependencies or install the generator package as the normal consumer route. Follow existing central package management when present. From the consuming project directory: -```xml - +```sh +dotnet add package Mammoth.LiteMapper ``` -Before relying on an advanced mapping, read [version boundaries](references/mapping-rules.md#version-boundaries). In particular, the stricter `IgnoreCase` ambiguity check, corrected dictionary/set comparer selection, and nullable enum-path fixes are unreleased. Do not promise them for `3.0.0` or invent a published next version. +Use the application's selected release version from its configured package feed; pass `--version` when pinning a specific version. Installing this skill does not change the project's package references. The documented support matrix is `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0`, with C# 9+ and a Roslyn 4.8.0+ compiler host. Shipping assemblies target `netstandard2.0`. Newer model syntax such as `required` still needs the corresponding compiler capabilities. ## Declare and call a mapping -Adapt this complete example from the Basic sample's instance mapping: +Adapt this complete instance-mapping example: ```csharp using Mammoth.LiteMapper; @@ -55,9 +55,9 @@ Call `new InstanceMapper().Map(new InstanceSource { Id = 42 })`. Static mappers ## Choose the relevant mapping rules -Read [references/mapping-rules.md](references/mapping-rules.md) for member configuration, construction, converter precedence and defaults, null policies, collections, patch updates, enums, cycles, or diagnostic troubleshooting. It includes source links and version-specific verification limits. +Read [references/mapping-rules.md](references/mapping-rules.md) for member configuration, construction, converter precedence and defaults, null policies, collections, patch updates, enums, cycles, or diagnostic troubleshooting. It includes compatibility guidance. -When upgrading from 1.x or 2.x, re-check any reliance on implicit root null guards, convention-only handwritten helpers, interface collection result types, tuple member names, former diagnostic IDs, or unmapped target members. In 3.0.0, root guards are opt-in through `GuardNonNullSource`, handwritten helpers need an explicit eligibility mechanism, equal-arity tuples map positionally, read-only sequence interfaces use array results, ordinary unmapped target members default to `LITEMAPPER1001` errors, and the specification's current diagnostic catalogue is canonical. +When upgrading, review [breaking-change migration](references/mapping-rules.md#breaking-change-migration) for changes that can affect compilation, null behavior, and collection equality. For simple matching, public instance properties and fields map by name. The default is exact matching followed by a unique case-insensitive match. Ordinary unmapped targets are errors; unmapped sources are ignored. Explicit `UnmappedTargetMembers` values (`Ignore`, `Info`, `Warning`, or `Error`) override that default, and standard compiler severity configuration can also relax configurable `LITEMAPPER1001`. That suppression or downgrade does not suppress valid generation and is not proof of complete mapping. Required, non-nullable, inaccessible, invalid, and otherwise mandatory targets still cause hard errors. `IgnoreTarget` and `UseTargetDefault` are explicit member-level opt-outs that retain their validation. @@ -73,4 +73,4 @@ Build the affected consumer project and inspect the actual `LITEMAPPER` diagnost Do not edit generated files, depend on private helper names or signatures, or assume extra public collection overloads exist. Call the mapping methods declared by the consumer. Identical non-collection reference types reuse the source reference unless an explicit mapping is selected; mutable collections are copied. -Use the [pinned upstream sources](references/mapping-rules.md#upstream-sources) for the installed release or the identified unreleased revision. Build examples against that package; report a mismatch rather than treating the newer documentation as proof of released behavior. +Build against the consumer's selected package and report any discrepancy with these bundled mapping rules rather than inventing alternate mapping semantics. diff --git a/skills/mammoth-litemapper/references/mapping-rules.md b/skills/mammoth-litemapper/references/mapping-rules.md index 1474257..e0115e7 100644 --- a/skills/mammoth-litemapper/references/mapping-rules.md +++ b/skills/mammoth-litemapper/references/mapping-rules.md @@ -1,22 +1,9 @@ # Mapping rules -Consumer guidance for published `3.0.0`, with unreleased corrections explicitly identified below. Use the [pinned upstream sources](#upstream-sources) for the installed package. The specification defines the contract; samples and tests demonstrate what a particular revision implements. +Consumer mapping rules for the current Mammoth.LiteMapper release. Nested mapper classes may live in non-generic partial records, record structs, or interfaces; every containing type must be partial and the mapper itself must remain a class. For example, a partial record Container can contain a static partial Mapper class. Attribute enum constants are validated by value: `(NameMatching)3` means IgnoreCase and maps source value to target Value; undefined999 is rejected, including in assembly defaults. -## Version boundaries - -`3.0.0` is the published stable install used by this skill. Unreleased notes refer to development revision [f0d4bee](https://github.com/PrimordialCode/Mammoth.LiteMapper/commit/f0d4beedc1c0f0105c0e2a5c37c283969ac23f96), not an available next-version package. Preserve the consumer's package choice and do not infer that a skill update changes its generator. - -Before applying unreleased guidance, check these affected shapes: - -- `IgnoreCase`: `3.0.0` can choose an exact member from multiple case-insensitive matches; the correction rejects the ambiguity with `LITEMAPPER1004` (#28). -- Comparers: `3.0.0` can preserve a source set comparer after same-type custom conversion, collapsing mapped values that should be distinct. Dictionary value conversion can discard a key comparer, and same-type custom key conversion can retain it incorrectly. The corrections change membership, lookup, counts, and collision behavior (#31, #51). -- Set construction on `netstandard2.0`, nullable structural collection elements, and guarded nullable enum source paths need the corresponding unreleased fixes (#26, #27, #30, #48). The published enum generator can fail to compile a guarded path. -- Different entry policies for shared models, instance converters inside automatic helpers, omitted-source root converters, attributed partial mapper parts, cycle wrappers, and captured nullable patch-updater arguments have additional unreleased corrections (#21-25, #29, #47). - -For an affected `3.0.0` consumer, reproduce the case against that package and use an explicit handwritten mapping when needed. Consult the [changes since 3.0.0](https://github.com/PrimordialCode/Mammoth.LiteMapper/compare/3.0.0...f0d4beedc1c0f0105c0e2a5c37c283969ac23f96) before recommending an upgrade; no next release number is assigned here. - ## Members and construction Populate read-only fields/properties through a selected constructor. For example, Target(int value) can initialize readonly Value from source Value3. An ordinary unbound read-only value field follows UnmappedTargetMembers and retains its value; do not emit an illegal initializer assignment. Required/non-nullable obligations remain enforced. @@ -34,7 +21,7 @@ private static string MapDisplayName(Customer source) => source.FirstName + " " + source.LastName; ``` -This is the usage guide's explicit-member pattern; adapt the model names and ensure every destination member is satisfiable. Source paths contain readable public fields/properties only, with no calls, indexing, or expressions. Targets are direct member names, never dotted paths. Omitting `Source` with explicit `Use` selects the root source; the unreleased correction (#23) ensures automatic member matching cannot override that explicit selection. +Adapt the model names and ensure every destination member is satisfiable. Source paths contain readable public fields/properties only, with no calls, indexing, or expressions. Targets are direct member names, never dotted paths. Omitting `Source` with explicit `Use` selects the root source before automatic member matching; no same-name source member is required. `[IgnoreTarget(nameof(Target.Member))]` excludes a target assignment. `[IgnoreSource(nameof(Source.Member))]` controls unused-source reporting, not target assignment. `[UseTargetDefault(nameof(Target.Member))]` deliberately preserves a real initializer or optional constructor default; it cannot invent a default or satisfy an unsatisfied required contract. @@ -52,7 +39,7 @@ Assembly-level defaults and `[UseMapper]` registrations are compile-time configu An explicit `Use = nameof(Parse)` can select an accessible inherited handwritten converter, following normal C# member lookup. For example, a protected base `int Parse(string value) => int.Parse(value) + 1` maps `"12"` to `13`. Instance converters require an instance mapping method; private base methods are inaccessible. Mapper configuration is not implicitly inherited. -Unreleased (#22): automatic nested and collection mappings preserve the mapper instance for selected instance converters and mappings. Static entry methods use only static converters and mappings. The development Basic sample demonstrates separate constructor-provided prefixes for nested objects and collection elements. User-owned mutable state still requires consumer synchronization. +Automatic nested and collection mappings preserve the mapper instance for selected instance converters and mappings. Static entry methods use only static converters and mappings. User-owned mutable state still requires consumer synchronization. Register static containers, including closed constructed generic types, exposing an accessible supported synchronous converter or mapping, including two-parameter updates. A container with `int Parse(string value)` is usable even for unrelated mapping pairs; one with only `void Ping()` reports `LITEMAPPER0010`. Unbound generic containers and generic methods are unsupported. Missing types also report0010, while existing non-static containers report0011. Normal C# accessibility permits private converters from a nested mapper; assembly registration does not make inaccessible methods callable. @@ -64,7 +51,7 @@ A nullable reference or value converter result targeting a non-null member repor Generated eligible partial mappings are discovered automatically. Handwritten local methods require one of those explicit mechanisms or `[DefaultMapping]`; names such as `ToDto` or a matching scalar/structural signature alone do not make a helper eligible. Explicit registration opts in compatible methods from a static external container. Mark converters used for dictionary keys/values or collection elements with `[MappingConverter]` when they should participate automatically; marked converters outrank identity conversion. -Generated nested and collection helpers are private; do not depend on their names or signatures. Unreleased correction (#21): helpers consistently retain the calling entry's options and nullable shape. Recheck entries that use the same models with different policies when upgrading. +Generated nested and collection helpers are private; do not depend on their names or signatures. Each mapping retains the calling entry's effective options and nullable shape, including when multiple entries use the same model types. Resolve explicit `MapProperty.Use` first, then an explicit source path (completing immediately if directly assignable), local marked converters, the local member convention, local mappings, registered external converters, and registered external mappings, before language/enum/collection/structural conversion. A unique default selects among mapping methods within its stage; it never skips an earlier stage. @@ -96,22 +83,22 @@ Options resolve method `[MappingOptions]`, mapper `[LiteMapper]`, assembly `[Lit For an opt-in strict source-completeness boundary, set `UnmappedSourceMembers` to `Error` at mapper, method, or assembly scope. This reports unused readable source members as configurable `LITEMAPPER1003` diagnostics naming the member; use `Warning` or `Info` to stage adoption and `IgnoreSource` for intentional exclusions. The default remains `Ignore` because source models can contain fields that are not part of a given destination contract. Source completeness does not weaken target completeness or mandatory target errors. -Name matching uses ordinal comparisons. `Exact` accepts only an exact name; `ExactThenIgnoreCase` prefers exact and otherwise requires a unique case-insensitive match. Unreleased correction (#28): `IgnoreCase` rejects source `Value` plus `value` targeting `Value` with non-configurable `LITEMAPPER1004`, including updates and nested structural mappings. Published `3.0.0` incorrectly selects the exact member in this case. Explicit `MapProperty.Source` can select the intended member. Constructor matching already follows the unique-ignore-case contract; hidden members are resolved before matching. +Name matching uses ordinal comparisons. `Exact` accepts only an exact name; `ExactThenIgnoreCase` prefers exact and otherwise requires a unique case-insensitive match. `IgnoreCase` requires a unique case-insensitive match without exact priority: source `Value` plus `value` targeting `Value` reports non-configurable `LITEMAPPER1004`, including updates and nested structural mappings. Explicit `MapProperty.Source` can select the intended member. Constructor parameters follow the same policy; hidden members are resolved before matching. ## Nulls and updates - Nullable-to-non-null values default to a compile-time error. `NullableMismatchPolicy.Throw` requests runtime validation. Nullable source and nullable result preserve root null. - Preserve nullable collection element types during mapping. In patch updates, nullable value members also skip assignment when null under `IgnoreNullSourceMembers`; for example, null `bool?` preserves an existing `true`, and non-null `false` replaces it. - Non-null root parameters have no runtime guard by default. Enable `GuardNonNullSource` when a guard is intended. -- For collections, the specified contextual default preserves null only for nullable destinations. `NullCollectionStrategy.Empty` explicitly materializes an empty destination; `Preserve` requires nullable targets. Verify the installed version's generated result for null-sensitive cases. +- For collections, the specified contextual default preserves null only for nullable destinations. `NullCollectionStrategy.Empty` explicitly materializes an empty destination; `Preserve` requires nullable targets. - Reference updates mutate and return the same destination when a destination-returning signature is used. Runtime null passed to a non-null destination parameter throws. A nullable destination is allowed only in a destination-returning form capable of constructing a replacement; a nullable `void` destination is invalid. - Existing-target updates are non-transactional and deterministic: target members are evaluated and assigned in ordinal target-member name order. A later getter, converter, nested mapping, collection operation, or setter failure leaves earlier assignments applied and skips later members. A per-member source capture may avoid repeated getter/path evaluation but does not provide rollback. New-object construction remains separate. - For `Target Update(Source source, Target? destination)` with `Target(int value)` and writable `Value`, source `Value = 12` and destination null create `Target(12)` without assigning the constructor-bound member again. A subsequent call with that target and source `Value = 17` updates it and returns the same reference. Existing-target members must still be writable; successful construction alone does not make a read-only update legal. - `IgnoreNullSourceMembers` is update-only. Null direct values or null path segments leave the existing member unchanged, including collections. At method scope use `OptionState.Enabled`. - Configured nullable paths are evaluated once per segment. With `[MapProperty(Source = "Address.Code", Use = nameof(Normalize), Target = nameof(Target.Code))]`, a `string? Normalize(string?)` converter receives null when `Address` is null. A `string Normalize(string)` parameter instead reports `LITEMAPPER2001` under `Error` or receives a full-path runtime check under `Throw`, even for a nullable target. - Null-collection policy includes null introduced by an intermediate configured path. For nullable `Data` in path `Data.Items`, the contextual default rejects a non-null collection target, explicit `Error` rejects nullable traversal for either target annotation, `Preserve` requires a nullable target, and `Empty` materializes an empty destination. -- Writable nested members and collections are replaced by default. Do not infer merging or get-only collection population. Nested in-place mutation needs an explicit compatible update mapping and confirmation that the installed version supports the requested shape. -- A non-null get-only child can use a declared `void ApplyChild(ChildSource source, ChildTarget target)`: source Value3 updates the existing child to3 without replacing it. Explicit `Use` with a configured source selects a handwritten updater; a configured path such as `Other.Item` supplies that nested source. Class-level external registration precedes assembly-level registration: an updater adding20 wins over an assembly updater adding10, producing23 for source3. Equally eligible updaters in the same registration scope require disambiguation or report `LITEMAPPER3001`. Verify nullable and recursive combinations against the installed revision before relying on them. +- Writable nested members and collections are replaced by default. Do not infer merging or get-only collection population. Nested in-place mutation needs an explicit compatible update mapping. +- A non-null get-only child can use a declared `void ApplyChild(ChildSource source, ChildTarget target)`: source Value3 updates the existing child to3 without replacing it. Explicit `Use` with a configured source selects a handwritten updater; a configured path such as `Other.Item` supplies that nested source. Class-level external registration precedes assembly-level registration: an updater adding20 wins over an assembly updater adding10, producing23 for source3. Equally eligible updaters in the same registration scope require disambiguation or report `LITEMAPPER3001`. When explicitly selecting an overloaded nested updater with `MapProperty.Use`, identity source compatibility wins. For example, for a `ChildSource` member, `Chosen(ChildSource source, ChildTarget target)` wins over `Chosen(object source, ChildTarget target)`. If the selected overload adds `10`, source value `3` becomes target value `13`. @@ -119,29 +106,29 @@ For a writable non-null child, select the updater explicitly with `[MapProperty( Omitting `Source` in `[MapProperty(Target = nameof(Target.Child), Use = nameof(ApplyChild))]` passes the root source to `void ApplyChild(Source source, ChildTarget target)`. No source member named `Child` is required: the updater can copy root `Value == 3` into `target.Child.Value`. -Unreleased (#29, #47): patch guards and nested updater calls share one captured source value, and nullable value captures preserve the selected overload for automatic direct members and configured direct or dotted paths. Published `3.0.0` can read a direct getter again or call a competing non-nullable overload for a configured nullable value. +Patch guards and nested updater calls share one captured source value, so direct nullable getters are read once and null skips the updater. Nullable value captures preserve the selected overload for automatic direct members and configured direct or dotted source paths. Nested updater calls obey patch and nullability rules. With `IgnoreNullSourceMembers`, null `source.Child` skips the updater and preserves the destination. Under the default mismatch policy, nullable child to a non-null updater parameter reports `LITEMAPPER2001`; `Throw` identifies the member path. A returning updater may create and return a nullable writable child, and the parent assigns it. A nullable get-only child cannot store a returned replacement and reports `LITEMAPPER5005`. ## Collections, enums, and cycles -Unreleased (#30): arrays and lists can map to `HashSet` or `ISet` on `netstandard2.0` without requiring unavailable capacity constructors. Published `3.0.0` can fail compilation for this shape. `IReadOnlySet` still requires that interface to exist; constructor selection does not change the mapped contents. +Arrays and lists can map to `HashSet` or `ISet` on `netstandard2.0` through parameterless or comparer-only construction when capacity constructors are unavailable. `IReadOnlySet` requires that interface to exist. Capacity is used only when the exact accessible constructor is available; constructor selection does not change the mapped contents. Enum numeric policy applies to declared values and unmatched-name ByValue fallback: Unchecked300 to a byte-backed enum yields44; Checked retains overflow validation. Missing composite flag names may map through their atoms (target Read4 OR Write8 gives12), including signed high-bit source flags; unknown source bits throw. A matching composite name must equal the mapped atomic OR: target Both12 is valid, Both16 reports LITEMAPPER7002 and omits the mapping. Every ordinary source alias must resolve consistently: Known/Alias1 may map to Known/Alias9. Declare public top-level collection mappings explicitly, for example `public static partial List MapCustomers(Customer[] source);` inside a static mapper. Supported shapes include one-dimensional/jagged arrays, common generic sequence/list interfaces, `List`, sets, and dictionaries; `IReadOnlySet` requires the symbol in the compilation. -Mutable collections are copied; elements are not necessarily deep-cloned. Arbitrary enumerable sources are traversed once. Null elements must not be silently removed; nullable structural element preservation needs the unreleased correction (#26). Dictionary keys and values map separately; duplicate converted keys throw through normal `Add` semantics. Do not generate custom/immutable collection construction, queues, stacks, rectangular arrays, or async enumerables. +Mutable collections are copied; elements are not necessarily deep-cloned. Arbitrary enumerable sources are traversed once. Nullable structural elements preserve null without calling an object-mapping helper; null elements are not silently removed. Dictionary keys and values map separately; duplicate converted keys throw through normal `Add` semantics. Do not generate custom/immutable collection construction, queues, stacks, rectangular arrays, or async enumerables. -Unreleased comparer corrections (#31, #51): concrete set sources preserve compatible comparers only for identity element mappings. A selected custom element converter uses the destination default comparer even for identical element types, so `x` and `X` both survive under default string equality while repeated `x` collapses. Concrete dictionary sources preserve compatible key comparers independently of value conversion; selected custom key converters use the destination default comparer even for identical key types. Interface-declared sources do not expose a comparer for preservation. Recheck membership, counts, lookups, and key collisions when upgrading from `3.0.0`. +Concrete set sources preserve compatible comparers only for identity element mappings. A selected custom element converter uses the destination default comparer even for identical element types, so `x` and `X` both survive under default string equality while repeated `x` collapses. Concrete dictionary sources preserve compatible key comparers independently of value conversion; selected custom key converters use the destination default comparer even for identical key types. Interface-declared sources do not expose a comparer for preservation. -Interface destinations use `T[]` for `IEnumerable`, `IReadOnlyCollection`, and `IReadOnlyList`; `List` for `ICollection` and `IList`; `HashSet` for set interfaces; and `Dictionary` for dictionary interfaces. The same result types apply to nested members and `NullCollections.Empty`. In 3.0.0, regression coverage verifies array results, mutable-list results, independent copies, one-pass enumeration, and nested/null-to-empty mappings. +Interface destinations use `T[]` for `IEnumerable`, `IReadOnlyCollection`, and `IReadOnlyList`; `List` for `ICollection` and `IList`; `HashSet` for set interfaces; and `Dictionary` for dictionary interfaces. The same result types apply to nested members and `NullCollections.Empty`. -An unknown-count enumerable-to-array mapping may allocate a temporary growing buffer plus its final array under the approved section 23.3 exception. Temporary storage/allocation is O(n); enumeration and element conversion happen once. With a cheap, reliable count, allocate the final array directly. This also applies to array-backed interfaces. Do not promise destination-only allocations for unknown-count arrays. +An unknown-count enumerable-to-array mapping may allocate a temporary growing buffer plus its final array. Temporary storage/allocation is O(n); enumeration and element conversion happen once. With a cheap, reliable count, allocate the final array directly. This also applies to array-backed interfaces. Do not promise destination-only allocations for unknown-count arrays. Enum defaults are `EnumMappingStrategy.ByName`, `UnmatchedEnumValuePolicy.Error`, and `EnumNumericConversion.Checked`. Names match exactly. `Throw` permits unmatched names with runtime `ArgumentOutOfRangeException`; by-value conversion uses the independent enum numeric policy. Domain-specific enum semantics belong in a converter. -Unreleased (#27, #48): a missing reference intermediate in `Child.State` maps to null when the destination enum is nullable, even when `State` is non-nullable. By-name unknown values throw with the configured path and original value; by-value preserves null before numeric conversion. A nullable-input explicit converter still receives null. Published `3.0.0` can fail to compile the guarded path; use an explicit handwritten mapping for that version. +A missing reference intermediate in `Child.State` maps to null when the destination enum is nullable, even when `State` is non-nullable. By-name unknown values throw with the configured path and original value; by-value preserves null before numeric conversion. Explicit converters retain precedence, and a nullable-input converter receives a missing path as null. `ReferenceHandling.ThrowOnCycle` detects active-path reference cycles in recursive generated mappings and throws `LiteMapperCycleException`. Shared references on separate branches are not cycles and identity is not preserved. Without tracking, recursive runtime cycles can overflow the stack. The exception exposes `SourceType`, `DestinationType`, `MappingMethod`, and `MemberPath`. @@ -149,11 +136,11 @@ Recursive declared mappings share one tracker. For an `A -> B -> A` component im Equal-arity tuple mappings are positional. `(int Number, string Text)` can map to `(long Id, string Label)` as `(7L, "seven")`; element names do not affect matching. Automatic tuple-to-object and object-to-tuple structural mapping reports `LITEMAPPER2004`. -## Diagnostics and source authority +## Diagnostics -Use the emitted diagnostic message and the matching specification §20 catalogue. Families: `0xxx` declarations/configuration, `1xxx` members/construction, `2xxx` nullability/conversions, `3xxx` mapping resolution, `4xxx` collections, `5xxx` updates, `6xxx` recursion, `7xxx` enums, `9001` internal generator failure. For an internal failure, reduce the case and report it without including proprietary data. +Use the emitted diagnostic message to identify the affected declaration or member. Families: `0xxx` declarations/configuration, `1xxx` members/construction, `2xxx` nullability/conversions, `3xxx` mapping resolution, `4xxx` collections, `5xxx` updates, `6xxx` recursion, `7xxx` enums, `9001` internal generator failure. For an internal failure, reduce the case and report it without including proprietary data. -In 3.0.0, a method-specific fatal error leaves that method unimplemented but does not suppress independent valid mappings in the same mapper. Invalid mapper-wide configuration suppresses that mapper; unrelated mappers continue. Fix the reported error before expecting the consumer build to succeed. +A method-specific fatal error leaves that method unimplemented but does not suppress independent valid mappings in the same mapper. Invalid mapper-wide configuration suppresses that mapper; unrelated mappers continue. Fix the reported error before expecting the consumer build to succeed. Async mapping declarations report `LITEMAPPER0006`. For example, change `async partial Target Map(Source source);` to `partial Target Map(Source source);` and perform asynchronous work before mapping. Task-returning mappings and async converters remain unsupported. @@ -161,26 +148,22 @@ Ordinary unmapped policies support `Ignore`, `Info`, `Warning`, and `Error`. Sta Projects requiring mandatory unmapped-target coverage should treat `LITEMAPPER1001` as an error in CI and audit or avoid project-wide suppression. The library cannot prevent an intentional consumer `.editorconfig` or other compiler severity override while this diagnostic remains configurable. -For configuration troubleshooting, use `1006` invalid source path, `1007` dotted target, `1008` duplicate target mapping, `1014` missing requested default, `1015` invalid ignored member, `1016` unsupported indexer selection, `2009` invalid selected converter signature, and `2011` ambiguous converters (all prefixed `LITEMAPPER`). Existing-target array updates report `4005`; get-only collection updates report `4004`. IDs `4003`, `6002`, and `6003` are not part of the 3.0.0 catalogue. +For configuration troubleshooting, use `1006` invalid source path, `1007` dotted target, `1008` duplicate target mapping, `1014` missing requested default, `1015` invalid ignored member, `1016` unsupported indexer selection, `2009` invalid selected converter signature, and `2011` ambiguous converters (all prefixed `LITEMAPPER`). Existing-target array updates report `4005`; get-only collection updates report `4004`. IDs `4003`, `6002`, and `6003` are not part of the diagnostic catalogue. Unexpected planning failures normally report one sanitized `LITEMAPPER9001` per failing mapper. The documented `LiteMapper_TreatInternalGeneratorErrorsAsExceptions` development option exposes full exception details through compiler generator-failure reporting; redact local paths and proprietary details before sharing them. To enable a diagnostic build property, also export it through a `CompilerVisibleProperty` item. For example, pair `true` in `PropertyGroup` with `` in `ItemGroup`. Apply the same pattern to `LiteMapper_IncludeGeneratedSourceComments` or `LiteMapper_TreatInternalGeneratorErrorsAsExceptions`; setting the property alone does not pass it to Roslyn. +## Breaking-change migration + +- From 1.x or 2.x, check reliance on implicit root null guards, convention-only handwritten helpers, interface collection result types, tuple member names, former diagnostic IDs, and unmapped targets. Root guards require `GuardNonNullSource`, helpers require an explicit eligibility mechanism, equal-arity tuples map positionally, read-only sequence interfaces produce arrays, and ordinary unmapped targets default to `LITEMAPPER1001` errors. +- From 3.0.0, ambiguous `IgnoreCase` mappings that previously selected an exact member fail with `LITEMAPPER1004`. Select the source with `MapProperty.Source`, or use `Exact`/`ExactThenIgnoreCase` when exact-name priority is intended. +- From 3.0.0, corrected comparer selection can change collection equality behavior. Dictionary value-only conversions preserve compatible source key comparers; custom key or set-element conversions use destination default comparers. Verify lookups, membership, element counts, and duplicate-key exceptions. +- From 3.0.0, recheck entries sharing model pairs with distinct conversion, null, enum, or cycle policies. Corrected policy isolation can change results and expose previously masked `LITEMAPPER2001` or `LITEMAPPER2002` errors. Patch updates evaluate each guarded member getter once and preserve the selected nullable updater overload; review side-effectful getters and nullable/non-nullable updater overloads. +- From 3.0.0, explicit `MapProperty.Use` without `Source` consistently selects the root source. Recheck previously defaulted outputs, including under `UnmappedTargetMembers.Ignore`. In patch updates, an unrelated same-name nullable member no longer prevents the root converter from running. +- From 3.0.0, structural collection-element fallback preserves null for nullable element targets. With non-nullable element targets under `Throw`, that fallback rejects null with `InvalidOperationException` before helper/member access, rather than an incidental `NullReferenceException`; higher-precedence nullable-input converters still receive null. Nullable-root by-name enum failures now use the declared parameter name in `ParamName`. Recheck exception-type and `ParamName` assertions. + ## Validation boundaries - Representative package-consumer checks cover `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0` with C# 9, C# 14, and `latest`. Compiler-host checks cover Roslyn 4.8, 4.14, and 5.9. These representative cases do not prove every feature on every compiler host. - Native AOT is validated separately and requires platform linker prerequisites. For example, Linux publishing requires Clang and zlib development files. - -## Upstream sources - -Released `3.0.0` sources (these older documents do not include the unreleased corrections above): - -- [Specification](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/SPECIFICATION.md): sole product contract, especially §§5-18 and 20. -- [Usage guide](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/docs/USAGE.md): consumer patterns. -- [README](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/README.md): package quickstart. -- [Basic sample](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/samples/Mammoth.LiteMapper.Samples.Basic/Program.cs): static/instance, nested collection, and cycle examples. -- [Collections sample](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/samples/Mammoth.LiteMapper.Samples.Collections/Program.cs): collection mapping. -- [ASP.NET Core sample](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/3.0.0/samples/Mammoth.LiteMapper.Samples.AspNetCore/Program.cs): direct mapper call from an endpoint. - -For the unreleased corrections, use the [f0d4bee usage guide](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/f0d4beedc1c0f0105c0e2a5c37c283969ac23f96/docs/USAGE.md), [specification](https://github.com/PrimordialCode/Mammoth.LiteMapper/blob/f0d4beedc1c0f0105c0e2a5c37c283969ac23f96/SPECIFICATION.md), and [samples](https://github.com/PrimordialCode/Mammoth.LiteMapper/tree/f0d4beedc1c0f0105c0e2a5c37c283969ac23f96/samples). These links identify a source snapshot, not a published package.