packages feed

figment-0.2.0.0: ADRs/006-runExtract-design.md

# ADR 006 — runExtract design: two-pass decode with provenance reattachment

> **Superseded by ADR 012, which is in turn superseded by ADR 018.**


Status: Superseded by ADR 012 | Date: 2026-10-21 | Rule: §2.1, §2.8, §11

## Context
`runExtract :: FromJSON a => Tagged -> Either ConfigError a` is the
boundary from the merged tree back to a caller's record type. The
feasibility study §11 calls this Risk #1 and states that no bridge
is designed.

The load-bearing question (Gate 2(b)): does leaf-level provenance
(specifically `path`) survive the round-trip through aeson's FromJSON?

## Options considered

(a) Two-pass decode:
    1. Convert Tagged -> aeson Value, dropping metadata.
    2. Decode Value -> a with aeson's standard FromJSON.
    3. On DecodeError, re-walk the original Tagged tree to locate the
       failing path for the error message.
    Provenance is NOT threaded through decode; it is only available to
    error reporting, not to the decoded result.

(b) Custom FromJSON Tagged:
    Write a `FromJSON Tagged` instance that carries metadata through
    every Parser step. This preserves per-field provenance in the
    decoded result, but requires `a` to be built from Tagged, not from
    aeson Value — a new class, not FromJSON.

(c) Field-path reattachment post-decode:
    Decode to `a` via (a), then re-walk Tagged and `a` in parallel to
    reattach provenance to each leaf of `a`. Requires `a` to be
    reflectable via `Generic`, and requires a parallel data type that
    mirrors `a` with per-leaf Tagged values.

## Decision (proposed)
Option (a) for Phase 3. Rationale:
- Matches §2.1 (one canonical IR: Tagged is the IR, aeson Value is a
  wire form used only at the provider/decode edge).
- Option (b) is a new typeclass the study does not name; it is a much
  larger design than an ADR can commit to.
- Option (c) is Gate 2(b) work, not Phase 3 work — provenance
  reattachment requires Generic walkers of `a` that we do not have.
- Option (a) satisfies the §8.1 DecodeError contract: on failure, the
  error carries the failing Path, even if the successful result does
  not carry provenance per-leaf.

## Consequences
- Gate 2(b) is NOT satisfied by Phase 3. If provenance must survive
  decode, a future ADR chooses between (b) and (c).
- `DecodeError Path Text` gets its Path by a Tagged-tree walk on the
  failing field, not from aeson's internal parser state.
- `Figment.hs` exports `composeChecked` / `composeUnchecked` as
  specified in §8.2, both delegating to `runLayers` + `runExtract`.