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'spreview/1callback 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 implementspreview/1, when the tool is unregistered, whenpreview/1raises, or when it returns a non-binary.
D-19 guard stack (live leg)
Cairnloop.ToolRegistry.find_tool_module/1— unknown tool → structured fallbackCode.ensure_loaded?/1— module not loaded → structured fallbackfunction_exported?(mod, :preview, 1)— callback absent → structured fallback- Atom rehydration of JSONB string keys via
String.to_existing_atom/1+ rescue ArgumentError — NEVERString.to_atom/1(unbounded-atom DoS / VM kill — T-14-01) struct/2rehydration — never re-running the tool's cast/validate pipeline (avoids validation side-effects)try/rescuearoundmod.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):
- ✓ Nullable
rendered_consequenceandtitlecolumns added tocairnloop_tool_proposals(migration20260524120100_add_snapshot_cols_to_proposals.exs). - ✓ Both columns populated in
Cairnloop.Governance.propose/3from Phase 15 forward (Preview.render/1called at propose-time and result snapshotted — D15-14). - ✓ Approval and execution surfaces MUST read the snapshotted
rendered_consequenceandtitlecolumns — NEVER call livePreview.render/1from an approval or execution surface (D-16). The live leg is for the timeline preview only; approval trust facts must be immutable. - ✓ Test added asserting that the approval card shows the snapshotted consequence when it
diverges from the live registry description (regression gate —
preview_test.exsD15-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.
Summary
Functions
Total render function for a governed tool proposal.
Functions
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).