Root Cause Classification — Gate Consistency
Phase B — Root Cause Classification
Six diagnostic questions
Q1. Why did VERIFY_MARK report cut_readiness_ok=true while composer_enabled=false?
Because fn_iu_cut_preflight_validate (mig 055) checked only manifest correctness (axes A–D + E1..E7). It never read iu_core.composer_enabled. VERIFY_MARK delegates wholesale to preflight, so it inherited the blind spot.
Q2. Does fn_iu_cut_preflight_validate currently check the composer gate?
No (pre-fix). Live md5 914e26d61de0de914408af5cdc679c07 shows only 8 gates: A, B, C, D, E1..E7. There was no E8 / composer / runtime gate. Mig 056 adds E8 composer_gate_open.
Q3. Does APPROVE re-run preflight at p_apply=true time?
Yes, via delegation to the same function — but that function had no E8. So re-running preflight at apply time still missed the composer gate.
Pre-fix APPROVE also had no cut_request cross-binding — it could approve a manifest whose cut_request was already in cut_in_progress/cut_done/cut_failed.
Q4. Does fn_cut_apply re-run preflight immediately before CUT?
No (pre-fix). It transitioned mark_verified→cut_in_progress, then called fn_iu_op_cut. The alias delegates to fn_iu_cut_from_manifest, which does check composer (G7) and returns {ok:false, refusal_code:'composer_gate_closed', run_id:<v_run_id>}.
Q5. Why did fn_cut_apply write cut_run_id/cut_done_at/cut_done even though fn_iu_cut_from_manifest refused?
Pre-fix fn_cut_apply did no result inspection. It read v_alias_out->>'cut_run_id' (NULL — alias returns run_id key, not cut_run_id) and fell through to defensive alias_out->>'run_id' (the refusal's generated run_id). Then unconditionally:
UPDATE public.cut_request
SET cut_run_id = v_run_id,
cut_done_at = now()
WHERE cut_request_id = p_cut_request_id;
PERFORM public.fn_cut_request_transition(..., 'cut_done', ...);
So a refusal looked like a success, and the Agent had to use the legal cut_done→cut_failed transition to record the actual outcome.
Q6. Which exact function owns the failure-state bug?
fn_cut_apply(primary) — unconditionalcut_donewrite.fn_iu_cut_from_manifestgenerates arun_ideven on refusal (defensible — it's an audit run id, not a CUT run id) but callers must distinguish ok-true from ok-false. The wrapperfn_iu_op_cutechoes bothappliedandrefusal_code; the bug is purely in the consumer.
Q7. Is CUT itself wrong, or did CUT correctly refuse?
CUT (fn_iu_cut_from_manifest) was correct: it honoured G7 composer_gate. The upstream gates (VERIFY, APPROVE) failed to stop earlier because they didn't model E8. The downstream caller (fn_cut_apply) failed to read the refusal result.
Classification
root_cause:
primary:
class: verify_approve_cut_gate_inconsistency
detail: |
fn_iu_cut_preflight_validate was missing E8 composer_gate_open.
VERIFY/APPROVE therefore returned cut_readiness_ok=true while CUT would refuse.
secondary:
class: approve_rubber_stamp_design
detail: |
fn_iu_verify_mark(p_apply=true) re-ran the same preflight (no E8) and
had no cut_request cross-binding. fn_cut_verify_mark also ignored the
inner verdict and transitioned cut_request to mark_verified purely on
p_approve=true.
tertiary:
class: cut_apply_failure_state_bug
detail: |
fn_cut_apply persisted cut_run_id + cut_done_at and called
fn_cut_request_transition(cut_done) regardless of whether the alias
refused, leaving a phantom successful-cut state on cut_failed rows.
not_root:
- manifest_schema (Axis A/B/C/D + E1..E7 all PASS for Điều 39)
- unit_kind (axis_d_ok=true; mig 054 holds)
- Codex_execution (manifest produced correctly)
- DOT_copy (source_hash + bytes match)
- fn_iu_op_cut (echoes refusal_code + applied correctly)
- fn_iu_cut_from_manifest (G7 composer gate correctly refuses)
Strategy
Strict refusal at each gate. No vocab widening, no silent value mapping, no composer-gate weakening, no alias contract change. Single TX surgical patch on the three problem functions (fn_iu_cut_preflight_validate, fn_iu_verify_mark+fn_cut_verify_mark, fn_cut_apply).