Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

## Fixes

- Issue #25: cycle detection now forwards one context through acyclic object and collection bridges, retaining complete member paths without tracking the bridge objects. [#25](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/25)
- Issue #24: partial mappers with framework attributes or `UseMapper` on another part now generate once; genuine duplicate `LiteMapper` configuration still reports one fatal diagnostic. [#24](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/24)
- Issue #23: explicit omitted-`Source` converters receive the root object before automatic source-member matching in new-object, constructor-bound, and update mappings. Unrelated source names and patch getters no longer override that selection. [#23](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/23)
- Issue #22: generated nested and collection helpers preserve the calling mapper instance for stateful converters and mappings, while instance-free helpers remain static. Static entry methods no longer select instance-only mappings. [#22](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/22)
Expand Down
8 changes: 8 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Mammoth.LiteMapper Decision Log

## Issue #25: distinguish context carriers from tracked vertices (2026-10-09)

- Context: recursive-helper membership was also used to decide which helpers receive tracker arguments. An acyclic helper calling a recursive helper therefore referenced undeclared context locals. Broadening that same set would incorrectly track acyclic bridge objects.
- Decision: maintain separate recursive-vertex and context-forwarding sets. Identify mixed declared/helper components from declared-call seeds independently of pure helper cycles; take the reverse helper-call closure only for context forwarding. Recursive public roots use the same Enter/finally/Exit contract as recursive helpers, while acyclic roots only create the single context.
- Paths: public collection bodies forward an empty initial path. Member append handles an empty incoming path, avoiding a leading dot while retaining the public method name and existing member-based collection paths.
- Contract: sections 14.1, 15, 17.2-17.5, 22.14, and 28.6-28.7 already require these semantics. No public context API, reflection, generated mutable mapper state, or normative change is introduced.
- Evidence: failing-before-fix compile/runtime/source-structure cases cover object and collection bridges, shared siblings, direct/indirect cycles, mixed declared components, constructors/updates, stateful instance converters, value-type bridges, concurrent calls, and None/acyclic controls. Basic and package consumer fixtures exercise the same wrapped collection cycle.

## Issue #24: discover the marker-owning partial declaration once (2026-10-09)

- Context: broad attributed-type discovery sees the merged LiteMapper marker on every attributed partial part. Repeated planning yields duplicate diagnostics and source hint collisions that discard unrelated generated output.
Expand Down
8 changes: 8 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Mammoth.LiteMapper Implementation Plan

## Issue #25: cycle context through acyclic bridges (2026-10-09)

- Scope: separate cycle-context forwarding from active-path vertex tracking. Acyclic object, struct, and collection bridges carry the initiating entry's tracker, member path, and method name to recursive helpers without entering the tracker themselves.
- Test-first: 19 new cases on unchanged develop yielded 17 failures and two controls; an additional mixed declared/helper component test failed the first candidate's over-tracking assertion before its repair. The Basic sample independently reproduced CS0103 for all three missing context locals.
- Implementation: preserve structural recursive helper membership and separately seed mixed declared/helper components, then compute the callerward context closure. Only recursive reference vertices Enter/Exit, including a recursive public root; root collections start with an empty path and helper members append without a leading dot.
- Validation: 20 new generator cases, complete generator/runtime/integration suites, source samples, clean-package samples, all 12 framework/language consumers, trimming/AOT consumer coverage, available packaging checks, independent review, and ordinary exact-head PR CI. Record unavailable stages separately.
- Boundary: issue #25 only; no public API or specification semantics change. Stop at draft PR/human review; no merge, release, tag, package publication, or workflow dispatch.

## Issue #24: one discovery per partial mapper (2026-10-09)

- Scope: a single marker-owning declaration per mapper symbol, retaining the established incremental candidate identity. Other partial declarations may carry framework attributes or UseMapper registrations without duplicating planning, diagnostics, or generated source. Genuine duplicate LiteMapper markers remain invalid.
Expand Down
10 changes: 10 additions & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Mammoth.LiteMapper Status

## GitHub issue #25 cycle bridge checkpoint (2026-10-09)

- Base: isolated cloud worktree from develop a68bb3e97d6697da9ef06b7ac65e07ea7c6c42ed after PR #38 merged. Issue #25 had no blocked-by dependencies, sub-issues, or competing PRs. User-authorized exec:agent and ready:agent labels were added and read back. AgentStack CLI is absent; connector fallback is used without issue comments or claim writes.
- Red-first: the completed initial 19-case matrix on unchanged production produced 17 failures and two passing None/acyclic controls, zero skips. The twentieth mixed declared/helper regression then failed the first candidate's over-tracking assertion before correction. A separate Basic sample build failed with missing tracker/path/method-name CS0103 errors. A direct-root cleanup assertion also failed before finally handling was unified.
- Implementation: distinct recursive vertices and context carriers; backward helper context propagation; preserved mixed declared/helper components; one tracker at the public recursive entry; no Enter/Exit for acyclic bridge objects; complete collection-root and nested paths. No public API or specification change.
- Verified local generator/runtime evidence: Release solution build has zero warnings/errors. All 696 generator tests pass, including 20 new cases; runtime five and integration one pass, zero failures/skips. All three source sample smoke runs pass. Generated bridge paths, active-path boundaries, and tracker allocation counts are asserted and executed. Available packaging suite: 220 passed, two infrastructure failures, zero skips, including all 12 framework/language combinations, clean-package samples, package/API checks, usage validation, and benchmark smoke. Actual netstandard2.0 package-generated source was inspected and confirms the forwarding/tracking boundary and complete path. Independent review found no code or coverage blocker; CRLF-aware diff validation passes. Ordinary exact-head CI belongs in the draft PR.
- Package scope: normal clean-package samples, the 12 framework/language consumers, and trimmed/Native AOT package consumers now execute the wrapped object/array/list cycle and shared-sibling example.
- Environment: existing workspace SDK 10.0.401 and runtimes 8.0.31/9.0.20/10.0.12, workspace NuGet caches, and one-processor builds. PowerShell and clang are absent; 49 PowerShell-dependent cases are not run locally. Trimming and Native AOT fail at ILLink ComputeManagedAssemblies task-host creation (MSB4216 / Unix-domain SocketException 13), before publish behavior is established. No local trimming/AOT success is claimed. No dependency, warning, assertion, credential, or security setting was weakened.
- Boundary: issue #25 only, then draft PR/human review. No user-computer access, other issue, merge, release, tag push, package publication, or workflow dispatch.

## GitHub issue #24 partial-discovery checkpoint (2026-10-09)

- Base: isolated cloud worktree from develop 1583ddbcff4e14e677d92acba86d222567196824 after PR #37 merged. Issue #24 is open with no dependencies/sub-issues or competing PR. The user authorized one next issue; exec:agent and ready:agent were added and read back. AgentStack CLI is absent; authorized connector fallback is used without issue comments or claim writes.
Expand Down
2 changes: 2 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,8 @@ 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.

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.

Recursive components may pass through value types and declared mapping methods. For example, an `A -> B -> A` graph mapped by declared `MapA` and `MapB` methods uses one tracker; a cycle reports `MapA` and member path `B.A`. A class-to-struct-to-class path forwards the tracker through the struct without tracking or boxing the struct itself.
Expand Down
45 changes: 45 additions & 0 deletions samples/Mammoth.LiteMapper.Samples.Basic/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ private static void Main()
throw new InvalidOperationException("Instance mapping failed.");
}

var sharedNode = new NodeSource();
var wrapped = new WrappedSource { Envelope = new EnvelopeSource { Nodes = new[] { sharedNode, sharedNode } } };
var wrappedTarget = WrappedCycleMapper.Map(wrapped);
if (wrappedTarget.Envelope.Nodes.Count != 2 ||
ReferenceEquals(wrappedTarget.Envelope.Nodes[0], wrappedTarget.Envelope.Nodes[1]))
{
throw new InvalidOperationException("Wrapped mapping failed.");
}
sharedNode.Next = sharedNode;
try
{
WrappedCycleMapper.Map(wrapped);
throw new InvalidOperationException("Wrapped cycle detection failed.");
}
catch (LiteMapperCycleException error) when (
error.MappingMethod == "Map" && error.MemberPath == "Envelope.Nodes.Next")
{
}

var node = new NodeSource();
node.Next = node;
try
Expand Down Expand Up @@ -132,6 +151,32 @@ public sealed class FormattedChildTarget
public string Value { get; set; } = string.Empty;
}

[LiteMapper(ReferenceHandling = ReferenceHandling.ThrowOnCycle)]
public static partial class WrappedCycleMapper
{
public static partial WrappedTarget Map(WrappedSource source);
}

public sealed class WrappedSource
{
public EnvelopeSource Envelope { get; set; } = new EnvelopeSource();
}

public sealed class WrappedTarget
{
public EnvelopeTarget Envelope { get; set; } = new EnvelopeTarget();
}

public sealed class EnvelopeSource
{
public NodeSource[] Nodes { get; set; } = new NodeSource[0];
}

public sealed class EnvelopeTarget
{
public List<NodeTarget> Nodes { get; set; } = new List<NodeTarget>();
}

public sealed class NodeSource
{
public NodeSource? Next { get; set; }
Expand Down
Loading
Loading