# `StatifierPersistence.Execution.Linkage`
[🔗](https://github.com/riddler/statifier_persistence/blob/v0.18.0/lib/statifier_persistence/execution/linkage.ex#L1)

A durable subchart child's parent linkage: the reserved, package-owned
namespace inside an execution's `metadata` (ADR-0008 decision 2).

ADR-0006 decision 1 says this package never reads a metadata key to make
a decision. ADR-0008 decision 2 narrows that, and narrows it exactly
this far: linkage lives under one reserved top-level key -
`"statifier_persistence"`, this module's `reserved_key/0` - this package
reads *only* that key, and everything outside it stays as opaque as it
was: never read, never validated beyond shape, never merged into a blob.
ADR-0006 decision 2 is untouched: every value here is an identity (an execution
id, an invocation id, a content hash) and never personal data.

The stored shape, under the reserved key:

    %{
      "parent_execution_id" => "execution_42",
      "invoke_id" => "call",
      "child_index" => 0,
      "content_hash" => "sha256:...",
      # fan-out only, both absent otherwise
      "child_count" => 3,
      "policy" => "all"
    }

`content_hash` is the **mandatory** pin ADR-0008 decision 2 hardens into
contract: the same hash `Statifier.Machine.identity/1` produces for the
child's own chart, recorded a second time where the parent-child
relationship can see it. A child is resumed by whatever node picks it up,
and this is what stands between "resumed the workflow you started" and
"resumed a different workflow that happens to share an id" - without the
pin, a reused or collided execution id would let a node silently guard a
child's own load against the wrong chart's identity while still
believing it is answering the right parent.

`child_index` is the child's position in the list its invocation fanned
out over: `0` for an ordinary durable subchart, `0..child_count - 1` for
a fan-out. It is durably on the child rather than held by whatever
started it, which is what lets a completion arriving on a node that has
never seen the parent live be placed at its index (ADR-0008's sp-3n2
amendment, point 2).

## The two fan-out values

`child_count` and `policy` are the amendment's ordered set expressed
per-child: N, and how the invocation aggregates its N answers - `:all`
waits for every child, `:first_error` cancels the rest as soon as one
fails. Both are **absent** from the stored map for an ordinary durable
subchart, and absence is the discriminator `fan_out?/1` reads: a child
with no `child_count` answers its parent's door directly, exactly as
every child created before this feature does, and its stored metadata is
byte-identical to what that path has always written.

`child_count: 1` is therefore not "not a fan-out". It is the N=1 fan-out
the amendment's point 3 names - one shape read at N=1 or at N=1000 -
and it settles through the same path a larger one does.

Both values are identities in ADR-0006 decision 2's sense: a count and a
constant, never personal data.

# `policy`

```elixir
@type policy() :: :all | :first_error
```

How an invocation aggregates its children's answers (statifier_blocks
ADR-0009 decision 6): `:all` waits for every child, `:first_error`
cancels the remaining ones as soon as one fails.

# `reserved_key`

```elixir
@type reserved_key() :: String.t()
```

The reserved top-level metadata key this module owns.

# `t`

```elixir
@type t() :: %StatifierPersistence.Execution.Linkage{
  child_count: pos_integer() | nil,
  child_index: non_neg_integer(),
  content_hash: StatifierPersistence.Storage.Adapter.content_hash(),
  invoke_id: String.t(),
  parent_execution_id: StatifierPersistence.Storage.Adapter.execution_id(),
  policy: policy() | nil
}
```

# `child_execution_id`

```elixir
@spec child_execution_id(
  parent_execution_id :: StatifierPersistence.Storage.Adapter.execution_id(),
  invoke_id :: String.t(),
  child_index :: non_neg_integer()
) :: StatifierPersistence.Storage.Adapter.execution_id()
```

Derives a child execution id from its parent and invocation - the single
definition site of the id shape (see the plan's "Implementation
Approach"):

    parent_execution_id <> "/" <> invoke_id <> "/" <> Integer.to_string(child_index)

Deterministic in every input, which buys idempotency across ADR-0004
decision 3's at-least-once re-drive: the same crash-and-retry recomputes
the same id, so the adapter's atomic `:execution_exists` refusal is what
answers the re-drive rather than a second child being created. The
result strictly extends `parent_execution_id` as a string, which is the
acyclicity property the cascade in Phase 5 rests on: no execution can be its
own descendant, because every descendant's id is strictly longer than
its ancestor's.

# `fan_out?`

```elixir
@spec fan_out?(t()) :: boolean()
```

Whether this linkage's child is one of a fan-out's N.

The discriminator is the presence of `child_count`, not its value:
`child_count: 1` is the N=1 fan-out (ADR-0008's sp-3n2 amendment, point
3) and answers `true`, while an ordinary durable subchart carries no
count at all and answers `false`.

# `from_metadata`

```elixir
@spec from_metadata(StatifierPersistence.Storage.Adapter.metadata()) ::
  {:ok, t()} | :no_linkage
```

Reads a linkage back out of a stored execution's `metadata`.

`:no_linkage` for an execution whose metadata carries none - a `%{}` metadata
map, a host map with unrelated keys, or an adapter that does not store
metadata at all. Not an `{:error, _}`: having no parent is an ordinary
property of an execution, not a failure, and every execution this package has ever
created before this feature has none.

The two fan-out values are read when present. A reserved map carrying
one of them without the other, a `child_count` that is not a positive
integer, a `child_index` outside `0..child_count - 1`, or a `policy`
that is neither spelling is malformed rather than partially readable,
and answers `:no_linkage` - the same arm every other malformed reserved
map takes.

# `invocation_match`

```elixir
@spec invocation_match(
  parent_execution_id :: StatifierPersistence.Storage.Adapter.execution_id(),
  invoke_id :: String.t()
) :: StatifierPersistence.Storage.Adapter.metadata()
```

The same containment map, narrowed to one invocation - what a cancel of a
single invocation walks, rather than every child a parent has ever
started.

# `new`

```elixir
@spec new(
  parent_execution_id :: StatifierPersistence.Storage.Adapter.execution_id(),
  invoke_id :: String.t(),
  child_index :: non_neg_integer(),
  content_hash :: StatifierPersistence.Storage.Adapter.content_hash()
) :: t()
```

Builds a linkage struct from its four values. Does not derive a child execution
id and does not touch storage - `child_execution_id/3` and `to_metadata/1` do
that separately, so a caller that only needs the id shape never has to
fabricate a `content_hash` it does not yet have.

# `new`

```elixir
@spec new(
  parent_execution_id :: StatifierPersistence.Storage.Adapter.execution_id(),
  invoke_id :: String.t(),
  child_index :: non_neg_integer(),
  content_hash :: StatifierPersistence.Storage.Adapter.content_hash(),
  child_count :: pos_integer(),
  policy :: policy()
) :: t()
```

`new/4`'s fan-out form: the same four values plus the invocation's
`child_count` and its aggregation `policy`.

`child_index` must be inside `0..child_count - 1`; anything else is a
caller bug rather than a storage event, so it raises `ArgumentError` the
way a malformed writer option does elsewhere in this package.

# `parent_match`

```elixir
@spec parent_match(
  parent_execution_id :: StatifierPersistence.Storage.Adapter.execution_id()
) ::
  StatifierPersistence.Storage.Adapter.metadata()
```

The containment map a cascade queries `Storage.list_executions_by_metadata/2`
with to find every child of `parent_execution_id`, whatever invocation started
it and whatever its `child_index`.

# `reserved_key`

```elixir
@spec reserved_key() :: reserved_key()
```

The one definition site for the reserved metadata namespace.

# `to_metadata`

```elixir
@spec to_metadata(t()) :: StatifierPersistence.Storage.Adapter.metadata()
```

The reserved-namespace metadata map for `linkage`, string keys and
JSON-representable values only, so the Ecto adapter's `jsonb` check
passes and the map is safe to merge into a `metadata:` option unchanged.

`child_count` and `policy` appear only for a fan-out child, so a
non-fan-out linkage produces exactly the four-key map this function has
always produced.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
