KB-23AC

dot-iu-cutter v0.5 — Lifecycle Implementation Authoring · fn_iu_apply_edit_draft Patch Package (Bundle E) (doc 3 of 6)

19 min read Revision 1
dot-iu-cutterv0.5lifecycle-enactment-implementation-authoringfn-iu-apply-edit-draft-patchglobal-coupling-defect-fixper-anchor-lifecycle-lookupbase-version-enacted-refusalpre-patch-md5-22875ce2post-patch-md5-not-yet-computedbehavior-equivalent-when-all-draftdieu442026-05-20

dot-iu-cutter v0.5 — Lifecycle Implementation Authoring · fn_iu_apply_edit_draft Patch Package

doc 3 of 6 · 2026-05-20 · PATCH AUTHORED, NOT EXECUTED

phase             : G2 (continued) — Bundle E patch
outcome           : PASS — full patched function body authored;
                    pre/post-patch md5 capture defined;
                    diff isolated to one block (lifecycle check)
production_mutation : NONE

0. Why this patch exists

The current production public.fn_iu_apply_edit_draft body (md5 22875ce25b2e2d1751cc4f3d1757252e, 4144 chars, SECDEF) contains the following block (verbatim, lines 23-26 of body):

SELECT count(DISTINCT lifecycle_status), min(lifecycle_status)
  INTO v_lc_count, v_uv_lifecycle
  FROM public.unit_version;
IF v_lc_count != 1 OR v_uv_lifecycle IS NULL OR btrim(v_uv_lifecycle)='' THEN
  RETURN jsonb_build_object('status','lifecycle_ambiguous',
    'guidance','Not uniquely determined.',
    'next_action','stop_for_gpt_review');
END IF;

This is a GLOBAL count across ALL UV rows in the database. The function uses v_uv_lifecycle (the alphabetic MIN of distinct values) to stamp lifecycle_status on any NEW UV row it inserts.

Current production state: all 165 UV rows have lifecycle_status='draft', so v_lc_count=1, the check passes, and new edit-draft applications inherit 'draft'.

The instant Phase 7 enacts any UV (the 60 ICX-CONST anchor UVs), the table will have 2+ distinct lifecycle_status values, the check will fail, and every future fn_iu_apply_edit_draft call across the entire database will return 'lifecycle_ambiguous' and refuse to publish any edit. This is a HARD breakage of all in-place edit flows, unrelated to the IU being edited.

This patch corrects the coupling: the new UV row inherits the lifecycle_status of the IU's current version anchor (per-IU, deterministic). Additionally, the patch refuses to apply an edit draft when the base anchor is 'enacted' (forces the future supersede path instead).

1. Pre/post-patch fingerprints

pre_patch:
  proname     : fn_iu_apply_edit_draft
  args        : p_draft_id uuid, p_actor text, p_review_note text
  prosecdef   : true
  body_md5    : 22875ce25b2e2d1751cc4f3d1757252e
  body_length : 4144

post_patch:
  proname     : fn_iu_apply_edit_draft         (unchanged)
  args        : p_draft_id uuid, p_actor text, p_review_note text  (unchanged)
  prosecdef   : true                            (unchanged)
  body_md5    : (computed after CREATE OR REPLACE FUNCTION applies;
                 expected NOT EQUAL to pre_patch.body_md5)
  body_length : ≈4140 ± 30 chars (small net change; lines ±5)
backup_capture (execution macro will do this BEFORE applying patch):
  SELECT prosrc INTO :backup_body
    FROM pg_proc
   WHERE pronamespace='public'::regnamespace
     AND proname='fn_iu_apply_edit_draft';
  # store :backup_body in operational sidecar (encrypted) so rollback
  # can reconstruct the pre-patch CREATE OR REPLACE statement.

2. The minimal diff (intent)

DELETE  (4 lines around line 23-29 of body):
  SELECT count(DISTINCT lifecycle_status),min(lifecycle_status) INTO v_lc_count,v_uv_lifecycle FROM public.unit_version;
  IF v_lc_count!=1 OR v_uv_lifecycle IS NULL OR btrim(v_uv_lifecycle)='' THEN
    RETURN jsonb_build_object('status','lifecycle_ambiguous','guidance','Not uniquely determined.','next_action','stop_for_gpt_review');
  END IF;

INSERT  (replacement block, ~8 lines):
  v_uv_lifecycle := v_uv.lifecycle_status;
  IF v_uv_lifecycle IS NULL OR btrim(v_uv_lifecycle) = '' THEN
    RETURN jsonb_build_object('status','lifecycle_ambiguous',
      'guidance','Current version anchor has empty lifecycle_status.',
      'next_action','stop_for_gpt_review');
  END IF;
  IF v_uv_lifecycle = 'enacted' THEN
    RETURN jsonb_build_object('status','base_version_enacted',
      'guidance','Cannot apply edit-draft on enacted version; use supersede path.',
      'next_action','fn_iu_create_edit_draft_for_supersede');
  END IF;

UNUSED VARIABLE  (declared but never assigned in new body):
  v_lc_count int  -- can be removed from DECLARE block; OR kept harmless

NOTHING ELSE CHANGES.

3. Full patched function body (CREATE OR REPLACE)

CREATE OR REPLACE FUNCTION public.fn_iu_apply_edit_draft(
    p_draft_id    uuid,
    p_actor       text,
    p_review_note text
) RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, public
AS $$
DECLARE
    v_draft         record;
    v_iu            record;
    v_uv            record;
    v_hash          text;
    v_next_seq      int;
    v_new_uv_id     uuid;
    v_uv_lifecycle  text;
    v_stale_count   int;
    v_inv           jsonb;
    v_comment_body  text;
    v_comment_id    uuid;
BEGIN
    IF p_draft_id IS NULL THEN
        RETURN jsonb_build_object('status','invalid_input','field','draft_id',
            'guidance','Required.','next_action','fix_input');
    END IF;
    IF btrim(COALESCE(p_actor,'')) = '' THEN
        RETURN jsonb_build_object('status','invalid_input','field','actor',
            'guidance','Required.','next_action','fix_input');
    END IF;

    SELECT * INTO v_draft FROM public.unit_edit_draft WHERE id = p_draft_id;
    IF NOT FOUND THEN
        RETURN jsonb_build_object('status','draft_not_found','draft_id',p_draft_id,
            'guidance','Check draft_id.','next_action','verify_draft_id');
    END IF;
    IF v_draft.draft_status != 'open' THEN
        RETURN jsonb_build_object('status','draft_not_open','draft_id',p_draft_id,
            'current_status',v_draft.draft_status,
            'guidance','Only open.','next_action','fn_iu_create_edit_draft');
    END IF;

    SELECT * INTO v_iu FROM public.information_unit WHERE id = v_draft.unit_id FOR UPDATE;
    IF v_iu.version_anchor_ref != v_draft.base_version_ref THEN
        UPDATE public.unit_edit_draft SET draft_status='stale_base', stale_at=now()
         WHERE id = p_draft_id;
        RETURN jsonb_build_object('status','stale_base','draft_id',p_draft_id,
            'guidance','Base changed.','next_action','fn_iu_create_edit_draft');
    END IF;

    SELECT * INTO v_uv FROM public.unit_version WHERE id = v_iu.version_anchor_ref;

    v_hash := public.fn_content_hash(v_draft.draft_body);
    IF v_hash != v_draft.draft_content_hash THEN
        RETURN jsonb_build_object('status','draft_hash_mismatch','draft_id',p_draft_id,
            'guidance','Corrupted.','next_action','fn_iu_create_edit_draft');
    END IF;
    IF v_hash = v_uv.content_hash AND v_draft.draft_title IS NULL THEN
        RETURN jsonb_build_object('status','no_change',
            'guidance','Identical.','next_action','none');
    END IF;

    -- ─── PATCH BEGINS ──────────────────────────────────────────────
    -- Previously: SELECT count(DISTINCT lifecycle_status) FROM public.unit_version
    -- (global coupling that breaks once any UV is non-'draft'.)
    --
    -- Now: inherit lifecycle_status from the CURRENT version anchor
    -- (per-IU, deterministic). Refuse to publish on top of an enacted
    -- base — those edits must travel through a future supersede path.
    v_uv_lifecycle := v_uv.lifecycle_status;
    IF v_uv_lifecycle IS NULL OR btrim(v_uv_lifecycle) = '' THEN
        RETURN jsonb_build_object('status','lifecycle_ambiguous',
            'guidance','Current version anchor has empty lifecycle_status.',
            'next_action','stop_for_gpt_review');
    END IF;
    IF v_uv_lifecycle = 'enacted' THEN
        RETURN jsonb_build_object('status','base_version_enacted',
            'unit_id',v_iu.id,
            'canonical_address',v_iu.canonical_address,
            'guidance','Base version is enacted; in-place apply not allowed. Use supersede path.',
            'next_action','fn_iu_create_edit_draft_for_supersede');
    END IF;
    -- ─── PATCH ENDS ────────────────────────────────────────────────

    SELECT COALESCE(MAX(version_seq),0) + 1 INTO v_next_seq
      FROM public.unit_version WHERE unit_id = v_iu.id;

    PERFORM set_config('app.canonical_writer','fn_iu_apply_edit_draft', true);

    INSERT INTO public.unit_version (id, unit_id, body, content_hash, version_seq,
                                     lifecycle_status, created_by, created_at)
    VALUES (gen_random_uuid(), v_iu.id, v_draft.draft_body, v_hash, v_next_seq,
            v_uv_lifecycle, btrim(p_actor), now())
    RETURNING id INTO v_new_uv_id;

    UPDATE public.information_unit
       SET version_anchor_ref = v_new_uv_id,
           content_anchor_ref = v_new_uv_id::text,
           updated_at         = now(),
           updated_by         = btrim(p_actor),
           identity_profile   = CASE
                                  WHEN v_draft.draft_title IS NOT NULL
                                  THEN jsonb_set(identity_profile,'{title}', to_jsonb(v_draft.draft_title))
                                  ELSE identity_profile
                                END
     WHERE id = v_iu.id;

    UPDATE public.unit_edit_draft
       SET draft_status='applied', applied_at=now(),
           applied_by=btrim(p_actor), applied_version_ref=v_new_uv_id
     WHERE id = p_draft_id;

    UPDATE public.unit_edit_draft
       SET draft_status='stale_base', stale_at=now()
     WHERE unit_id = v_iu.id AND id != p_draft_id AND draft_status='open';
    GET DIAGNOSTICS v_stale_count = ROW_COUNT;

    v_comment_body := COALESCE(p_review_note,
                               format('Applied as v%s by %s', v_next_seq, btrim(p_actor)));
    INSERT INTO public.unit_edit_comment (draft_id, unit_id, author_ref, author_type,
                                          comment_body, comment_kind)
    VALUES (p_draft_id, v_iu.id, btrim(p_actor), 'system', v_comment_body, 'system')
    RETURNING id INTO v_comment_id;

    v_inv := public.fn_iu_verify_invariants(v_iu.canonical_address);
    IF NOT (v_inv->>'all_pass')::boolean THEN
        RAISE EXCEPTION 'invariant_failed: %', v_inv;
    END IF;

    RETURN jsonb_build_object(
        'status','applied',
        'draft_id',p_draft_id,
        'version_id',v_new_uv_id,
        'version_seq',v_next_seq,
        'unit_id',v_iu.id,
        'lifecycle_status',v_uv_lifecycle,
        'stale_drafts_count',v_stale_count,
        'comment_id',v_comment_id,
        'invariants',v_inv,
        'guidance','Published.',
        'next_action','none');
END;
$$;

COMMENT ON FUNCTION public.fn_iu_apply_edit_draft(uuid,text,text) IS
    'Apply an open edit-draft as a new unit_version. v0.5-M3a patch (2026-05-20): replaced global lifecycle_status coupling with per-anchor lookup; added base_version_enacted refusal to force supersede path on enacted bases.';

4. Behavior equivalence proof (when all UVs are 'draft')

preconditions (current production):
  - every UV row in public.unit_version has lifecycle_status='draft'
  - therefore v_uv.lifecycle_status = 'draft' for every v_iu

pre-patch behavior on a 'draft' base:
  1. SELECT count(DISTINCT lifecycle_status) FROM public.unit_version
     → returns (1, 'draft')
  2. v_lc_count=1, v_uv_lifecycle='draft' → check passes
  3. proceeds to insert new UV with lifecycle_status='draft'

post-patch behavior on a 'draft' base:
  1. v_uv_lifecycle := v_uv.lifecycle_status   → 'draft'
  2. v_uv_lifecycle is not NULL/empty          → check passes
  3. v_uv_lifecycle != 'enacted'               → check passes
  4. proceeds to insert new UV with lifecycle_status='draft'

⇒ pre-patch and post-patch behaviors are IDENTICAL for every existing
  use case in production today. The patch is a NO-OP from the caller's
  perspective until at least one UV enters a non-'draft' state.

5. Behavior differences (after at least one UV becomes non-'draft')

scenario A: edit-draft against a DRAFT base (the IU's anchor is draft):
  pre-patch:  v_lc_count > 1 (because some OTHER IU is enacted)
              → status='lifecycle_ambiguous'   ❌ HARD BREAK
  post-patch: v_uv.lifecycle_status='draft' on THIS IU
              → status='applied'                ✓ proceeds correctly

scenario B: edit-draft against an ENACTED base:
  pre-patch:  if no other UVs are enacted (count=1, but the value is
              'enacted'): proceeds with v_uv_lifecycle='enacted' →
              inserts NEW UV with lifecycle_status='enacted' (no
              enacted_at!) — silently FORGES an enacted version.
              ❌ SUBTLE CORRUPTION
              if some UVs are draft + some are enacted (count=2):
              → status='lifecycle_ambiguous'.
  post-patch: → status='base_version_enacted'  ✓ refused with clear
                next_action ('fn_iu_create_edit_draft_for_supersede')

⇒ post-patch behavior is strictly safer than pre-patch in BOTH
  partial-enacted scenarios.

6. Risk assessment

test_coverage_needed:
  T-I10 : edit-draft on draft IU still works (regression-pre-patch)
  T-I11 : edit-draft on an enacted IU returns 'base_version_enacted'
          (new behavior)
  T-I12 : edit-draft against a stale base still returns 'stale_base'
          (regression-pre-patch)
  T-I13 : edit-draft with hash mismatch still returns 'draft_hash_mismatch'

ROLLBACK-only verification before exposing to live traffic:
  - apply patch in a transaction
  - dump pg_get_functiondef('public.fn_iu_apply_edit_draft'::regproc)
  - diff against the documented body in this doc
  - ROLLBACK

callers known to invoke fn_iu_apply_edit_draft:
  - Directus server-side flow (workflow_admin role; via REST)
  - public.fn_iu_save (auto_apply policy path)
  - manual SELECT public.fn_iu_apply_edit_draft(...) ops calls

caller_impact:
  - return-shape additions: 'base_version_enacted' is a NEW status value;
    callers that switch on `status` MUST handle it or fall through
    safely (most do; the existing 'no_change' / 'stale_base' /
    'lifecycle_ambiguous' branches already require status-aware code).
  - return-shape stability: all existing status values are preserved
    ('invalid_input','draft_not_found','draft_not_open','stale_base',
     'draft_hash_mismatch','no_change','lifecycle_ambiguous','applied').

7. Pre-flight + post-patch verification probes

-- Pre-flight (run as context_pack_readonly):
SELECT md5(prosrc) AS pre_md5, length(prosrc) AS pre_len
  FROM pg_proc
 WHERE pronamespace='public'::regnamespace
   AND proname='fn_iu_apply_edit_draft';
-- expect: pre_md5 = '22875ce25b2e2d1751cc4f3d1757252e', pre_len = 4144

-- Post-patch (immediately after CREATE OR REPLACE FUNCTION):
SELECT md5(prosrc) AS post_md5, length(prosrc) AS post_len, prosecdef AS secdef
  FROM pg_proc
 WHERE pronamespace='public'::regnamespace
   AND proname='fn_iu_apply_edit_draft';
-- expect: post_md5 != pre_md5; secdef = true; length within ±100 of pre

ROLLBACK-only behavioral probe (run AFTER patch applies, BEFORE Phase 7 or any commit; assumes some test edit-draft exists OR using a manufactured one):

-- B-9 (post-patch, ROLLBACK-only): on a draft base, patch behavior unchanged
BEGIN;
-- (test setup: a known open draft id pointing to a draft IU base)
SELECT public.fn_iu_apply_edit_draft(
    '<known_draft_id>', 'verify_probe', 'B-9 probe');
-- expect: status='applied' OR 'no_change' OR 'draft_hash_mismatch'
--         (any of the existing draft-base success/refusal statuses)
ROLLBACK;

-- B-10 (post-patch, manufactured enacted base): refuses with base_version_enacted
-- (can only run AFTER Phase 7 ; during this macro we cannot construct
--  an enacted base without violating forbidden boundaries; left as a
--  follow-on integration test under LIFECYCLE_ENACT_TEST_INTEGRATION=1)

8. Rollback procedure for Bundle E

-- if the patched function needs to be reverted:
-- 1. Restore from the operational sidecar :backup_body captured pre-patch:

CREATE OR REPLACE FUNCTION public.fn_iu_apply_edit_draft(
    p_draft_id uuid, p_actor text, p_review_note text
) RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, public
AS $body$
<insert-prior-body-text-here>
$body$;

-- 2. Verify md5 matches pre-patch:

SELECT md5(prosrc) FROM pg_proc
 WHERE pronamespace='public'::regnamespace
   AND proname='fn_iu_apply_edit_draft';
-- expect: '22875ce25b2e2d1751cc4f3d1757252e'
rollback_constraint:
  - rollback is SAFE only when no UV has been enacted yet (i.e. before
    Phase 7).
  - if rollback fires AFTER any UV is enacted: the pre-patch body's
    global coupling will start returning 'lifecycle_ambiguous' for
    every call. Caller surface degrades; consider whether rollback is
    actually the right move vs forward-fix.

9. Patch ordering vs other bundles

constraint:
  Bundle E MUST be applied BEFORE Phase 7 (the 60-IU enactment macro).
  WITHOUT Bundle E, Phase 7 will leave fn_iu_apply_edit_draft in a
  hard-broken state for every IU in the database.

flexibility:
  Bundle E can be applied:
    - BEFORE Bundles A..D (it has no dependency on iu_lifecycle_vocab/log
      or fn_iu_enact);
    - INTERLEAVED with Bundles A..D in either order;
    - or AS THE LAST Bundle, immediately before Phase 7 runs.
  Recommended ordering (design doc 05 §2):
    Bundle A → B → C → D → E
  Rationale: the function chain (vocab/log → triggers → enact → policy →
  patch) reads top-down as a story; not a hard constraint.

10. G2 Bundle E disposition

G2_Bundle_E_fn_iu_apply_edit_draft_patch : PASS
production_mutation                       : NONE
delivered:
  - exact diff identified
  - full patched function body authored
  - behavior-equivalence proof (current state)
  - behavior-difference table (post-Phase-7 state)
  - risk assessment + test coverage
  - pre/post fingerprint capture
  - rollback procedure
  - patch ordering analysis
next:
  - G3 — verification + rollback + compensation plan
    [[dot-iu-cutter-v0-5-04-verification-rollback-compensation-plan-2026-05-20]]

Related KB documents in this package:

  • [[dot-iu-cutter-v0-5-01-live-recheck-and-scope-lock-2026-05-20]]
  • [[dot-iu-cutter-v0-5-02-fn-iu-enact-ddl-package-2026-05-20]]
  • [[dot-iu-cutter-v0-5-04-verification-rollback-compensation-plan-2026-05-20]]
  • [[dot-iu-cutter-v0-5-05-command-review-package-2026-05-20]]
  • [[dot-iu-cutter-v0-5-06-final-implementation-authoring-report-2026-05-20]]
Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.5-lifecycle-enactment-implementation-authoring/dot-iu-cutter-v0.5-03-fn-iu-apply-edit-draft-patch-package-2026-05-20.md