# `Cairnloop.Governance.Preview`
[🔗](https://github.com/szTheory/cairnloop/blob/main/lib/cairnloop/governance/preview.ex#L1)

Total `render/1` function for governed tool proposals — hides the live-vs-fallback
branching behind a single public API.

## Return values

- `{:preview, String.t()}` — the tool's `preview/1` callback returned a non-empty
  binary string. The string is best-effort LIVE prose (labelled "current description"
  in the UI); it MAY diverge from the prose that was current at propose-time.
- `{:structured, map()}` — the COMMON Phase-14 path. Built ENTIRELY from the
  propose-time snapshot. No live registry read for the structured fields (trust
  correctness). Used when no tool implements `preview/1`, when the tool is
  unregistered, when `preview/1` raises, or when it returns a non-binary.

## D-19 guard stack (live leg)

1. `Cairnloop.ToolRegistry.find_tool_module/1` — unknown tool → structured fallback
2. `Code.ensure_loaded?/1` — module not loaded → structured fallback
3. `function_exported?(mod, :preview, 1)` — callback absent → structured fallback
4. Atom rehydration of JSONB string keys via `String.to_existing_atom/1` + rescue
   ArgumentError — NEVER `String.to_atom/1` (unbounded-atom DoS / VM kill — T-14-01)
5. `struct/2` rehydration — never re-running the tool's cast/validate pipeline (avoids validation side-effects)
6. `try/rescue` around `mod.preview(input_struct)` — bad host tool degrades ONE card,
   never crashes the LiveView

## D-17 common path

No tool in Phase 14 implements `preview/1`, so `{:structured, _}` is the expected
result for all proposals in this phase.

## Phase 15 forward-compat guardrail — DISCHARGED

The D-16 4-step mandate has been completed in Phase 15 (Plan 15-01):

1. ✓ Nullable `rendered_consequence` and `title` columns added to `cairnloop_tool_proposals`
   (migration `20260524120100_add_snapshot_cols_to_proposals.exs`).
2. ✓ Both columns populated in `Cairnloop.Governance.propose/3` from Phase 15 forward
   (`Preview.render/1` called at propose-time and result snapshotted — D15-14).
3. ✓ Approval and execution surfaces MUST read the snapshotted `rendered_consequence` and
   `title` columns — NEVER call live `Preview.render/1` from an approval or execution surface
   (D-16). The live leg is for the timeline preview only; approval trust facts must be immutable.
4. ✓ Test added asserting that the approval card shows the snapshotted consequence when it
   diverges from the live registry description (regression gate — `preview_test.exs` D15-14 block).

Failure to snapshot at propose-time means approval surfaces will silently show different
prose after a tool implementation changes — a trust and audit correctness failure. This guard
remains here as the discoverable marker for future phases.

# `render`

Total render function for a governed tool proposal.

Returns `{:preview, String.t()}` if the live `preview/1` leg succeeds,
or `{:structured, map()}` as the COMMON fallback (D-17).

The structured result is built from the propose-time snapshot only — no live
config re-read for trust fields. The title fallback chain uses the live
`__tool_spec__/0` Spec title only if the module is loaded (best-effort).

---

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