# Variants and Variant Artifacts

> Design note. The current user-facing DSL and local publication workflow are
> documented in [README.md](README.md). Some snippets below are conceptual and
> describe model direction rather than exact public API.

## Overview

This document describes the artifact model built on top of `variants` and
`variantSources`.

The goal is to define:

- a stable artifact-facing model for outgoing contracts;
- a DSL for declaring slots and their inputs;
- a resolver bridge between `variantArtifacts` and `variantSources`;
- extension points for deduplication and similar policies;
- a model that remains live during configuration and does not require an
  artificial freeze of slot content.

The design follows the same split already used elsewhere:

- `variants` defines the closed domain topology;
- `variantSources` defines source materialization semantics over that topology;
- `variantArtifacts` defines outgoing artifact contracts over that topology.

---

## Core idea

`variantArtifacts` is not the owner of the variant model and not the owner of
source-set materialization.

Its purpose is narrower:

- decide which variants participate in outgoing publication;
- define artifact slots for those outgoing variants;
- declare how each slot gathers its inputs.

This makes `variantArtifacts` an outgoing-contract layer, not a compilation or
source-materialization layer.

---

## Model boundaries

### What belongs to `variants`

- identities: `Variant`, `Role`, `Layer`;
- normalized topology relation `(variant, role, layer)`;
- finalization of the core domain model;
- `VariantsView` and derived topology views.

### What belongs to `variantSources`

- compile units and role projections derived from finalized `variants`;
- source-set naming policy;
- source-set materialization;
- lazy access to `GenericSourceSet` providers and their named outputs.

### What belongs to `variantArtifacts`

- selection of outgoing variants;
- slot identities inside an outgoing variant;
- slot assembly declarations;
- root outgoing configurations for outgoing variants;
- slot-level assembly bodies;
- publication-facing hooks.

### What does not belong to `variantArtifacts`

- ownership of variant existence;
- ownership of source-set naming;
- direct mutation of source materialization internals;
- compiler- or toolchain-specific logic;
- eager flattening of source inputs into files during DSL declaration.

---

## Outgoing subset

Not every declared `Variant` must become outgoing.

`variantArtifacts` defines an outgoing subset of variants.

For each outgoing variant there is one root outgoing aggregate:

- one root consumable `Configuration`;
- one or more artifact slots;
- one primary slot;
- optional secondary slots.

This is why `OutgoingConfiguration` is a real model object and not merely a
publication event payload.

It represents a live build-facing aggregate:

- it has its own attributes;
- it is visible to other build logic as soon as registered;
- it contains lazy handles rather than eagerly materialized state;
- it is distinct from slot assembly state.

---

## Identity-first split

The artifact model should preserve the same identity-first principle used for
the rest of the project.

### Identity objects

- `Variant`
- `Slot`
- `ArtifactSlot = (variant, slot)`

These objects are:

- cheap;
- replayable;
- suitable for discovery and selection.

### Stateful objects

- `OutgoingConfiguration`
- `ArtifactAssembly`

These objects are:

- build-facing;
- allowed to contain Gradle lazy handles;
- obtained through dedicated APIs rather than embedded into identity objects.

### Rule of thumb

- slot identity is cheap and replayable;
- inside one `OutgoingConfiguration`, `Slot` is the natural local identity;
- `ArtifactSlot` is useful when slot identity must be referenced outside the
  parent outgoing configuration;
- slot body is stateful and resolved separately;
- outgoing configuration is a live aggregate, not a mere snapshot.

---

## Artifact model

Conceptually:

```java
interface VariantArtifactsContext {
    VariantsView getVariants();
    void all(Action<? super OutgoingConfiguration> action);
    Optional<OutgoingConfiguration> findArtifacts(Variant variant);
    OutgoingConfiguration requireArtifacts(Variant variant);
    ArtifactAssemblies getAssemblies();
}
```

```java
interface OutgoingConfiguration {
    Variant getVariant();
    NamedDomainObjectProvider<Configuration> getOutgoingConfiguration();
    NamedDomainObjectContainer<Slot> getSlots();
    Property<Slot> getPrimarySlot();
}
```

```java
interface ArtifactAssemblies {
    ArtifactAssembly resolveSlot(ArtifactSlot slot);
}
```

This intentionally uses a Gradle-style local slot container rather than a
separate `ArtifactSlotsView`.

The reason is simple:

- inside one `OutgoingConfiguration`, slot identity is local and naturally
  expressed as `Slot`;
- a dedicated `ArtifactSlotsView` would suggest a detached readonly projection
  without clearly owning mutation/configuration semantics;
- `NamedDomainObjectContainer<Slot>` better matches the live configuration model
  and keeps the parent aggregate as the owner of slot structure.

`ArtifactSlot` remains useful, but only as a fully qualified identity outside
the parent aggregate:

- resolver APIs;
- global payloads;
- cross-variant references.

Important distinction:

- `OutgoingConfiguration` describes outgoing publication structure;
- `ArtifactAssembly` describes how one slot artifact is assembled.

An `ArtifactAssembly` does not create the root outgoing configuration. It serves
one already declared slot inside that configuration.

### Primary slot ownership

Primary status belongs to the parent outgoing configuration, not to the slot
itself.

This is why the preferred container-level contract is:

```java
Property<Slot> getPrimarySlot();
```

and not a slot-local boolean or a derived `ArtifactSlot` reference.

Reasons:

- the primary role is assigned by the parent aggregate;
- within one `OutgoingConfiguration`, the variant identity is already known, so
  `Slot` is sufficient and avoids redundant `(variant, slot)` duplication;
- `Property` matches the rest of the Gradle-facing live model better than a
  custom `findPrimarySlot()` style API;
- `Property` gives useful write/configure/finalize semantics without inventing a
  separate special lifecycle abstraction.

Expected usage:

- DSL or adapters may assign the primary slot through `set(...)`;
- adapters that want to provide a default may use `convention(...)`;
- the property may remain unset while the model is still incomplete;
- at materialization time the property may be finalized;
- if more than one slot exists and `primarySlot` is still unset at the
  materialization point, that is a model error.

The model should still enforce that the selected primary slot belongs to the
same `OutgoingConfiguration`.

### Proposed public shape

With these constraints, the preferred public structure is:

```java
interface VariantArtifactsContext {
    VariantsView getVariants();
    void all(Action<? super OutgoingConfiguration> action);
    Optional<OutgoingConfiguration> findArtifacts(Variant variant);
    OutgoingConfiguration requireArtifacts(Variant variant);
    ArtifactAssemblies getAssemblies();
}

interface OutgoingConfiguration {
    Variant getVariant();
    NamedDomainObjectProvider<Configuration> getOutgoingConfiguration();
    NamedDomainObjectContainer<Slot> getSlots();
    Property<Slot> getPrimarySlot();
}

interface ArtifactAssemblies {
    ArtifactAssembly resolveSlot(ArtifactSlot slot);
}

record ArtifactSlot(Variant variant, Slot slot) {}
```

This gives:

- one global registry of outgoing variants;
- one local slot container per outgoing variant;
- one fully qualified slot identity for resolver and cross-aggregate use;
- one explicit service for slot assembly materialization.

### Minimal internal shape

The preferred minimal internal structure is:

```java
final class VariantArtifactsRegistry implements VariantArtifactsContext {
    OutgoingConfiguration outgoingConfiguration(Variant variant);
    ArtifactAssemblyRules slotRules(ArtifactSlot slot);
    ArtifactAssemblies assemblies();
}

interface ArtifactAssemblyRules {
    void from(Object input);
    <T extends Task> void producedBy(
        TaskProvider<T> task,
        Function<? super T, ? extends Provider<? extends FileSystemLocation>> output);
    void fromVariant(Action<? super OutputSelectionSpec> action);
    void fromRole(String roleName, Action<? super OutputSelectionSpec> action);
    void fromLayer(String layerName, Action<? super OutputSelectionSpec> action);
}
```

Responsibility split:

- `VariantArtifactsRegistry` owns the whole artifact model;
- `OutgoingConfiguration` owns only the structural variant-local aggregate;
- `ArtifactAssemblyRules` owns the content declaration of one slot;
- `ArtifactAssemblies` materializes `ArtifactAssembly` from
  `ArtifactSlot -> ArtifactAssemblyRules`.

This keeps `OutgoingConfiguration` focused on structure and avoids overloading it
with slot-content APIs such as `rules(slot)`.

### DSL binding

The DSL should be connected through the registry, not by making
`OutgoingConfiguration` responsible for content rules.

Conceptually:

```java
variantArtifacts.variant("browser", spec -> {
    spec.slot("runtime", assembly -> {
        assembly.fromRole("production", out -> out.output("js"));
    });
});
```

Operationally this means:

1. registry creates or returns `OutgoingConfiguration` for the variant;
2. slot declaration creates or returns `Slot` inside that outgoing configuration;
3. registry forms `ArtifactSlot(variant, slot)`;
4. registry resolves `slotRules(artifactSlot)`;
5. bound `ArtifactAssemblySpec` writes into those rules.

So the DSL writes:

- structure into `OutgoingConfiguration`;
- slot content into registry-owned `ArtifactAssemblyRules`.

This is the intended bridge point between the public DSL and the internal
resolver/materialization model.

---

## Live model and monotonic structure

`variantArtifacts` should be treated as a live configuration model during the
whole configuration phase.

This means:

- slot inputs remain live;
- `from(...)`, `fromVariant(...)`, `fromRole(...)`, `fromLayer(...)` may keep
  contributing inputs until task execution;
- `producedBy(...)` publishes an existing task output directly and does not
  create the managed copy assembly for that slot;
- `ArtifactAssembly` may expose live `FileCollection`, `Provider`, and task
  wiring;
- external task outputs remain outside the control of this model and must be
  accepted as live inputs.

The model should therefore avoid a mandatory freeze phase for slot content.

Instead, it should follow a monotonic rule:

- outgoing variant existence may grow;
- slot existence may grow;
- slot content may grow;
- publication-visible identity should not be retroactively redefined.

In practice this means:

- slot names are stable once declared;
- primary slot designation is structural;
- slot input content remains live.

This also means that the model does not need a dedicated freeze phase for slot
content merely because the root outgoing configuration was registered earlier.

Early registration of the root `Configuration` and live evolution of slot input
content are compatible concerns.

---

## DSL principles

The DSL should remain declarative and symbolic.

It should describe:

- which variant is outgoing;
- which slots exist;
- which slot is primary;
- which selectors contribute inputs to each slot.

It should not directly expose:

- `GenericSourceSet`;
- `FileCollection`;
- concrete resolved files from `variantSources`;
- internal resolver state.

### DSL shape

Conceptually:

```groovy
variantArtifacts {
    variant("browser") {
        primarySlot("runtime") {
            fromRole("production") {
                output("js")
                output("resources")
            }
        }

        slot("types") {
            fromVariant {
                output("dts")
            }
        }

        slot("sources") {
            fromLayer("main") {
                output("sources")
            }
        }

        slot("bundleMetadata") {
            producedBy(writePackageMetadata) {
                outputFile
            }
        }
    }
}
```

### Meaning of contribution forms

- `from(Object)` adds a direct input independent from `variantSources`;
- `fromVariant { output(...) }` selects named outputs from all compile units of
  the current variant;
- `fromRole(role) { output(...) }` selects named outputs from compile units that
  belong to the given role projection;
- `fromLayer(layer) { output(...) }` selects named outputs from the compile unit
  of the current variant and the given layer, if such unit exists.
- `producedBy(task) { outputFile }` maps an existing producing task to the single
  file or directory published for the slot.

Contribution forms and `producedBy(...)` are mutually exclusive for one slot.
The DSL stores declarations, not resolved file collections.

---

## Contribution model

Internally the DSL should compile to slot contributions.

Conceptually:

- `DirectContribution`
- `VariantOutputContribution`
- `RoleOutputContribution`
- `LayerOutputContribution`

These contributions should remain symbolic for as long as possible.

They should not resolve source sets or files at declaration time.

Each contribution is expected to provide:

- its selection scope;
- the requested output names;
- enough symbolic identity for later validation and resolver policies.

---

## Resolver bridge between `variantSources` and `variantArtifacts`

This is the central integration point.

`variantArtifacts` should not access mutable internals of `variantSources`.

Instead, it should resolve slot inputs through the public finalized
`VariantSourcesContext`.

### Bridge responsibilities

The bridge:

- takes slot contribution declarations;
- expands them against finalized variant topology;
- maps logical selectors to compile units and role projections;
- obtains source sets lazily through `VariantSourcesContext`;
- resolves named outputs from those source sets;
- builds the live input model for an `ArtifactAssembly`.

### Bridge input

- current outgoing variant identity;
- slot contribution declarations;
- `VariantSourcesContext`.

### Bridge output

- a live collection of logical slot inputs;
- later adapted to `FileCollection` or other assembly-facing input models.

### Resolution semantics

For one outgoing variant:

- `fromVariant { output(x) }`
  - expands to all `CompileUnit` of that variant;
- `fromRole(role) { output(x) }`
  - expands to `RoleProjection(variant, role)` and then to its compile units;
- `fromLayer(layer) { output(x) }`
  - expands to one compile unit `(variant, layer)` when it exists;
- `from(Object)`
  - bypasses `variantSources` completely.
- `producedBy(task)`
  - bypasses contribution resolution and registers the task output as the slot
    artifact directly.

After compile units are known, the bridge asks
`ctx.getSourceSets().getSourceSet(unit)` for each selected unit and resolves the
requested named output.

This keeps `variantArtifacts` independent from source-set naming internals and
other materialization details.

---

## Validation

Validation should be structural and symbolic.

It should validate:

- outgoing variant refers to an existing `Variant`;
- referenced `Role` exists in that variant projection space;
- referenced `Layer` exists in that variant compile-unit space;
- primary slot is defined when needed;
- primary slot refers to a slot declared in the same outgoing configuration.

Validation should not require eager materialization of source sets or eager
resolution of files.

---

## Deduplication and policy extension points

Deduplication is important, but it should not be baked into the DSL itself.

The correct place for it is the resolver bridge, after symbolic contributions
have been expanded to logical inputs but before they are finally adapted to
assembly-facing file collections.

### Why not in the DSL

At declaration time it is still unknown whether selectors overlap:

- `fromVariant`
- `fromRole`
- `fromLayer`

may all describe the same logical source output.

### Why not rely only on `FileCollection`

`FileCollection` may still provide useful physical deduplication, but it is too
late and too file-oriented to serve as the only semantic mechanism.

The artifact model should first deduplicate logical inputs, then let Gradle
perform any additional physical deduplication.

### Default expectation

The default resolver should support:

- deduplication of topology-aware inputs by logical identity;
- no implicit deduplication of direct `from(Object)` inputs.

Logical identity should be based on domain meaning, for example:

- `(CompileUnit, outputName)`

and not on projected source-set names.

This is important because source-set naming policy belongs to `variantSources`
and must not silently redefine artifact semantics.

### Extension points

The model should provide explicit internal extension points for:

- deduplication policy;
- logical input identity;
- adaptation of resolved logical inputs to assembly-facing objects.

Conceptually:

```java
interface SlotInputDedupPolicy { ... }
interface LogicalSlotInputIdentity { ... }
interface SlotInputAdapter { ... }
```

The default implementation may remain simple, but these seams should exist from
the start.

---

## Publication hooks

Publication hooks remain useful, but they should observe the live structural
model rather than define it.

Examples:

- `whenOutgoingConfiguration(...)`
- `whenOutgoingSlot(...)`

These hooks are adapter-facing customization points over already declared
outgoing structure:

- root configuration attributes;
- slot artifact attributes;
- assembly task tweaks.

The recommended way to connect publication-facing `Spec` objects to the
structural model is by backlink, not by moving slot rules into publication
types.

Conceptually:

```java
interface OutgoingConfigurationSpec {
    OutgoingConfiguration getOutgoingArtifacts();
    Variant getVariant();
    Configuration getConfiguration();
}

interface OutgoingArtifactSlotSpec {
    ArtifactSlot getArtifactSlot();
    ArtifactAssembly getAssembly();
    boolean isPrimary();
}
```

In this arrangement:

- `OutgoingConfigurationSpec` remains a publication-facing facade;
- `OutgoingConfigurationSpec` may expose the structural aggregate when an
  adapter needs it;
- slot rules still belong to `VariantArtifactsRegistry`;
- publication specs do not become owners of declaration or resolver state.

They should not become the primary structural API for the artifact model.

This is why a separate phase-oriented `OutgoingPublicationsContext` is not
required.

The live `OutgoingConfiguration` aggregate is sufficient.

---

## Design principles

### 1. Keep topology ownership in `variants`

`variantArtifacts` selects from the topology model. It does not own it.

### 2. Keep source ownership in `variantSources`

`variantArtifacts` consumes source materialization through a resolver bridge. It
does not own source-set semantics.

### 3. Keep the DSL symbolic

The DSL declares intent and selection rules, not materialized files.

### 4. Keep slot content live

Do not introduce an artificial finalize phase for slot content unless a real
semantic need appears.

### 5. Fix only structural identity

Slot name, primary designation, and outgoing shape are structural. Slot inputs
remain live.

### 6. Resolve through dedicated bridges

Cross-model integration belongs in a resolver service, not in DSL classes.

### 7. Add policy seams early

Deduplication and similar concerns should have extension points from the start,
even if the initial implementation is conservative.

---

## Summary

`variantArtifacts` should be modeled as a live outgoing-contract layer over
`variants`, with source input resolution delegated to a dedicated bridge over
`variantSources`.

The resulting shape is:

- `variants` owns topology;
- `variantSources` owns source materialization;
- `variantArtifacts` owns outgoing contract structure;
- a resolver bridge connects symbolic slot declarations to live source-derived
  inputs;
- deduplication and similar concerns are policies of that bridge, not of the
  DSL itself;
- slot content stays live during configuration;
- only publication-visible structure is treated as stable identity.
