KB-16EA

fn_cut_apply Failure-State Bug Fix

5 min read Revision 1
iu-cutcut_applyfailure-statecut_donecut_failedmig056

Phase E — fn_cut_apply failure-state bug fix

md5

ce488b884200893e38ffb17bca4baceb007e20974e25a3e7dd8f9884800a3637

Pre-fix behavior (the bug)

-- pre-fix
PERFORM public.fn_cut_request_transition(p_cut_request_id, 'cut_in_progress', ...);
v_alias_out := public.fn_iu_op_cut(...);
v_run_id := (v_alias_out->>'cut_run_id')::uuid;       -- always NULL (wrong key name)
IF v_run_id IS NULL THEN
  v_run_id := (v_alias_out->>'run_id')::uuid;         -- gets the audit run_id even on refusal
END IF;
UPDATE public.cut_request SET cut_run_id  = v_run_id, cut_done_at = now() WHERE ...;
PERFORM public.fn_cut_request_transition(p_cut_request_id, 'cut_done', ...);

When fn_iu_op_cut returned {ok:false, refusal_code:'composer_gate_closed', run_id:<uuid>} (because fn_iu_cut_from_manifest G7 refused), fn_cut_apply still wrote cut_run_id + cut_done_at and transitioned to cut_done. The Agent then had to use the legal cut_done→cut_failed transition to record the actual state — producing a phantom successful-cut audit trail on a failed request.

Post-fix behavior

Three changes:

1. Re-run preflight BEFORE transitioning to cut_in_progress

v_preflight := public.fn_iu_cut_preflight_validate(v_req.manifest_staging_record_id, NULL, p_actor);
v_pre_ready := COALESCE((v_preflight->>'cut_readiness_ok')::boolean, false);
IF NOT v_pre_ready THEN
  INSERT INTO public.dot_iu_command_run (...) VALUES ('dot_cut_apply', ..., 'refused', ..., 'preflight_refused', ...);
  RETURN jsonb_build_object(
    'ok', false, 'verdict', 'refused',
    'cut_request_id', p_cut_request_id,
    'refusal_code', 'preflight_refused',
    'status', v_req.status,
    'preflight', v_preflight);
END IF;

If composer is closed (or any other preflight gate fails), the request stays at mark_verified rather than landing in cut_in_progress just to fall to cut_failed. Cleaner state machine.

2. Inspect alias result — never trust silence

v_alias_out := public.fn_iu_op_cut(...);
v_inner     := v_alias_out->'inner_result';
v_inner_ok  := COALESCE((v_inner->>'ok')::boolean, false);
v_applied   := COALESCE((v_inner->>'applied')::boolean, false);
v_refusal   := v_inner->>'refusal_code';

IF NOT v_inner_ok OR (p_apply AND NOT v_applied) THEN
  -- Alias refused. Do NOT write cut_run_id/cut_done_at. cut_in_progress -> cut_failed.
  PERFORM public.fn_cut_request_transition(p_cut_request_id, 'cut_failed', p_actor, ...);
  PERFORM public.fn_cut_request_signal('cut.cut_failed', ...);
  RETURN jsonb_build_object('ok',false,'verdict','refused','refusal_code',v_refusal,
                            'status','cut_failed','alias_result',v_alias_out, ...);
END IF;

The wrapper looks INTO inner_result.ok and inner_result.applied (the keys actually produced by fn_iu_cut_from_manifest). No more dependency on cut_run_id / run_id key fishing.

3. Persist cut_run_id + cut_done_at ONLY on success

v_run_id := COALESCE((v_alias_out->>'run_id')::uuid, (v_inner->>'run_id')::uuid);
UPDATE public.cut_request SET cut_run_id = v_run_id, cut_done_at = now() WHERE ...;
PERFORM public.fn_cut_request_transition(p_cut_request_id, 'cut_done', p_actor, ...);

These three lines are now guarded by the IF NOT v_inner_ok OR (p_apply AND NOT v_applied) early-return above. No phantom cut_done going forward.

Live verification (T4, inside BEGIN/ROLLBACK)

Setup: revert cut_request to mark_verified with cut_run_id=NULL, cut_done_at=NULL. Call fn_cut_apply(c7133284…, true, 'regression_T4') with composer=false.

output value
ok false
verdict refused
refusal_code preflight_refused
status mark_verified (no transition to cut_in_progress!)
post cut_request.status mark_verified
post cut_request.cut_run_id NULL
post cut_request.cut_done_at NULL
IU count delta 0 (still 200 total)

State-machine paths covered

from event to
mark_verified preflight refused at fn_cut_apply mark_verified (no transition; return refusal)
mark_verified preflight ok → fn_iu_op_cut refuses cut_in_progress → cut_failed (refusal_code threaded into metadata)
mark_verified preflight ok → fn_iu_op_cut applies cut_in_progress → cut_done (cut_run_id + cut_done_at persisted)

Pinned md5 of unchanged neighbours

function md5
fn_iu_op_cut 66b813e50205448eb01170aebec614df
fn_iu_cut_from_manifest c5d556bc22cc2d255c0484b5a969ebc5
fn_cut_request_transition c392d3e156159852a97889df1af04165
Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-cut-verify-approve-cut-gate-consistency-fix/05-cut-apply-failure-state-fix.md