Cairnloop.Governance.Preview (cairnloop v0.1.0)

Copy Markdown View Source

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.

Summary

Functions

Total render function for a governed tool proposal.

Functions

render(proposal)

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).