KB-6498 rev 2

23-P3C1 — Edit Draft Safe Functions — Execution Prompt

15 min read Revision 2
pack-23p3c1promptfunctionsedit-plancreate-draftcommentnaturalsafe

23-P3C1 — Edit Draft Safe Functions — Execution Prompt

Date: 2026-05-07 Author: Opus (Claude) Status: PROMPT — chờ GPT/User review. CHƯA dispatch. Scope: CREATE 4 safe functions (no IU/UV writes). P3C2 sẽ cover apply + wrapper. Report: knowledge/dev/laws/dieu44-trien-khai/reports/23-p3c1-iu-edit-draft-safe-functions-report.md


Triết lý

  • Khi an toàn → tự làm.
  • Khi có nguy cơ nhầm → dừng nhẹ nhàng, chỉ đúng lựa chọn, đưa nút tiếp theo.
  • Response tự dạy AI cách làm. README chỉ 3 dòng.

Agent UX Contract

Quy tắc Ý nghĩa
1 high-level function per intent Sửa → fn_iu_edit. Góp ý → fn_iu_comment. Xem trước → fn_iu_edit_plan.
Agent không cần biết table, version_seq, hash, gateway, anchor, birth, draft_id (trừ low-level)
1 target an toàn → làm luôn 1 open draft → attach comment
Nhiều target → không đoán >1 open draft → return candidates + guidance
Mọi response có guidance {status, guidance, next_action}
Low-level cho advanced flows fn_iu_comment_edit_draft khi cần exact draft_id

Status Vocabulary

Status Meaning Required fields Guidance
plan_ok Dry-run thành công unit_id, base_seq, hash, policy Ready to create draft or edit
invalid_input Input sai field, message Fix input
iu_not_found Address không tồn tại address Check canonical_address
no_head_version IU chưa có version unit_id IU needs initial version via fn_iu_create
no_change Body giống hiện tại Nothing to change
draft_created Draft tạo thành công draft_id, unit_id, base_seq To apply: fn_iu_apply_edit_draft
comment_added Comment đã ghi comment_id, draft_id
no_open_draft Không có draft nào open To suggest edit: fn_iu_edit
multiple_open_drafts Nhiều draft, không đoán drafts[] (id, author, created_at) Choose draft_id or pass context select=latest
draft_not_found draft_id không tồn tại Check draft_id

(Statuses cho P3C2: applied, stale_base, draft_not_open, policy_require_review, invariant_failed.)


Functions (P3C1 — 4 safe functions)

Exact Signatures

fn_iu_edit_plan(p_address text, p_body text, p_actor text) RETURNS jsonb
fn_iu_create_edit_draft(p_address text, p_body text, p_actor text, p_reason text DEFAULT NULL, p_title text DEFAULT NULL) RETURNS jsonb
fn_iu_comment_edit_draft(p_draft_id uuid, p_author text, p_body text, p_kind text DEFAULT 'general', p_author_type text DEFAULT 'agent') RETURNS jsonb
fn_iu_comment(p_address text, p_author text, p_body text, p_kind text DEFAULT 'general', p_author_type text DEFAULT 'agent', p_context jsonb DEFAULT '{}'::jsonb) RETURNS jsonb

All: SECURITY DEFINER, SET search_path=pg_catalog,public, REVOKE ALL FROM PUBLIC, GRANT EXECUTE TO {discovered role}.


Setup

#!/usr/bin/env bash
CONTAINER="${PG_CONTAINER:-postgres}"
DB="${PG_DB:-directus}"
DBUSER="${PG_USER:-directus}"
PSQL=(docker exec -i "$CONTAINER" psql -U "$DBUSER" -d "$DB" -v ON_ERROR_STOP=1)
PSQL_NOSTOP=(docker exec -i "$CONTAINER" psql -U "$DBUSER" -d "$DB")
TS=$(date +%Y%m%d-%H%M%S)
LOG="/tmp/23-p3c1.${TS}.log"
exec > >(tee -a "$LOG") 2>&1

PREFLIGHT_STATUS=""
FN_STATUS="NOT_RUN"
TEST_FAIL=0
PHASE_STATUS=""; P3C2_READINESS="BLOCKED"
IU_COUNT_BEFORE=""; UV_COUNT_BEFORE=""
DRAFT_COUNT_BEFORE=""; COMMENT_COUNT_BEFORE=""
FN_OWNER=""
UV_LIFECYCLE_VALUE=""

echo "=== P3C1 START $TS ==="

§1. Preflight

echo "=== PREFLIGHT ==="

# Gate 1: P3B tables
for TBL in unit_edit_draft unit_edit_comment; do
  E=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_tables WHERE schemaname='public' AND tablename='$TBL';" | tr -d ' ')
  [ "$E" = "1" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: $TBL missing"; }
done

# Gate 2: P3A gateway
if [ -z "$PREFLIGHT_STATUS" ]; then
  GW=$("${PSQL[@]}" -t -A -c "SELECT value FROM dot_config WHERE key='iu_create.gateway.allowed_marker_values';")
  [ "$GW" = "fn_iu_create,fn_iu_apply_edit_draft" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: gateway"; }
fi

# Gate 3: Pack 22 functions
if [ -z "$PREFLIGHT_STATUS" ]; then
  for FN in fn_iu_create fn_iu_verify_invariants fn_content_hash; do
    FC=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname='$FN';" | tr -d ' ')
    [ "$FC" -ge "1" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: $FN missing"; }
  done
fi

# Gate 4: Target functions must NOT exist
if [ -z "$PREFLIGHT_STATUS" ]; then
  for FN in fn_iu_edit_plan fn_iu_create_edit_draft fn_iu_comment_edit_draft fn_iu_comment; do
    FC=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname='$FN';" | tr -d ' ')
    [ "$FC" = "0" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: $FN exists"; }
  done
fi

# Gate 5: P3C2 functions also must NOT exist (clean state)
if [ -z "$PREFLIGHT_STATUS" ]; then
  for FN in fn_iu_apply_edit_draft fn_iu_edit; do
    FC=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname='$FN';" | tr -d ' ')
    [ "$FC" = "0" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: $FN exists (P3C2 not yet)"; }
  done
fi

# Gate 6: UV lifecycle convention from data
if [ -z "$PREFLIGHT_STATUS" ]; then
  UV_LIFECYCLE_VALUE=$("${PSQL[@]}" -t -A -c "SELECT DISTINCT lifecycle_status FROM unit_version;")
  UV_LIFECYCLE_COUNT=$(echo "$UV_LIFECYCLE_VALUE" | grep -c . 2>/dev/null || echo "0")
  echo "UV_LIFECYCLE=$UV_LIFECYCLE_VALUE count=$UV_LIFECYCLE_COUNT"
  if [ "$UV_LIFECYCLE_COUNT" = "1" ]; then
    echo "UV_LIFECYCLE_CONVENTION=$UV_LIFECYCLE_VALUE (unanimous from data)"
  elif [ "$UV_LIFECYCLE_COUNT" = "0" ]; then
    PREFLIGHT_STATUS="FAIL"; echo "FAIL: no UV rows to determine convention"
  else
    # Mixed — inspect fn_iu_create source
    echo "UV_LIFECYCLE mixed — Agent must inspect fn_iu_create source"
    FN_SRC=$("${PSQL[@]}" -t -A -c "SELECT prosrc FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname='fn_iu_create';")
    echo "$FN_SRC" > "/tmp/p3c1-fn_iu_create-source-${TS}.txt"
    echo "Source saved to /tmp/p3c1-fn_iu_create-source-${TS}.txt"
    # If still unclear → STOP
  fi
fi

# Gate 7: Function owner pattern
if [ -z "$PREFLIGHT_STATUS" ]; then
  FN_OWNER=$("${PSQL[@]}" -t -A -c "SELECT proowner::regrole FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname='fn_iu_create';")
  echo "FN_OWNER=$FN_OWNER"
  [ -n "$FN_OWNER" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: no fn owner"; }
  # Also discover GRANT pattern
  FN_GRANTS=$("${PSQL[@]}" -t -A -c "SELECT proacl FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname='fn_iu_create';")
  echo "FN_GRANTS_PATTERN=$FN_GRANTS"
fi

# Gate 8: Counts
if [ -z "$PREFLIGHT_STATUS" ]; then
  IU_COUNT_BEFORE=$("${PSQL[@]}" -t -c "SELECT count(*) FROM information_unit;" | tr -d ' ')
  UV_COUNT_BEFORE=$("${PSQL[@]}" -t -c "SELECT count(*) FROM unit_version;" | tr -d ' ')
  DRAFT_COUNT_BEFORE=$("${PSQL[@]}" -t -c "SELECT count(*) FROM unit_edit_draft;" | tr -d ' ')
  COMMENT_COUNT_BEFORE=$("${PSQL[@]}" -t -c "SELECT count(*) FROM unit_edit_comment;" | tr -d ' ')
  echo "IU=$IU_COUNT_BEFORE UV=$UV_COUNT_BEFORE DRAFT=$DRAFT_COUNT_BEFORE COMMENT=$COMMENT_COUNT_BEFORE"
fi

# Gate 9: Get one existing IU address for tests
if [ -z "$PREFLIGHT_STATUS" ]; then
  TEST_IU_ADDR=$("${PSQL[@]}" -t -A -c "SELECT canonical_address FROM information_unit LIMIT 1;")
  TEST_IU_BODY=$("${PSQL[@]}" -t -A -c "
    SELECT uv.body FROM unit_version uv
    JOIN information_unit iu ON iu.version_anchor_ref = uv.id
    WHERE iu.canonical_address = '$TEST_IU_ADDR';
  ")
  echo "TEST_IU=$TEST_IU_ADDR BODY_LEN=${#TEST_IU_BODY}"
fi

if [ -z "$PREFLIGHT_STATUS" ]; then PREFLIGHT_STATUS="PASS"; fi
echo "PREFLIGHT_STATUS=$PREFLIGHT_STATUS"
if [ "$PREFLIGHT_STATUS" != "PASS" ]; then
  FN_STATUS="NOT_RUN"; PHASE_STATUS="FAIL"; P3C2_READINESS="BLOCKED"
fi

§2. Create Functions

Agent: đọc fn_iu_create source + UV lifecycle convention trước khi viết. Viết 4 functions theo signatures và logic dưới đây.

fn_iu_edit_plan(p_address text, p_body text, p_actor text) → jsonb

Read-only. Logic:

  1. Validate non-empty: address, body, actor → {status:'invalid_input', field, guidance}
  2. Resolve address → IU → iu_not_found nếu missing
  3. HEAD UV → no_head_version nếu NULL
  4. Hash via fn_content_hash(p_body) → no_change nếu trùng
  5. Count open drafts cho unit_id
  6. Read policy
  7. {status:'plan_ok', unit_id, current_version_seq, base_content_hash, draft_content_hash, open_draft_count, policy, auto_apply_eligible}

fn_iu_create_edit_draft(p_address, p_body, p_actor, p_reason, p_title) → jsonb

Logic:

  1. Validate
  2. Resolve address → IU + HEAD UV
  3. canonical_address = IU.canonical_address (from DB, not p_address)
  4. Hash, no-change check
  5. INSERT unit_edit_draft
  6. {status:'draft_created', draft_id, unit_id, canonical_address, base_version_seq, guidance:'To apply: SELECT fn_iu_apply_edit_draft(draft_id, actor)'}

fn_iu_comment_edit_draft(p_draft_id, p_author, p_body, p_kind, p_author_type) → jsonb

Low-level. Logic:

  1. Validate (including kind IN allow-list, author_type IN allow-list)
  2. Load draft → draft_not_found
  3. Resolve unit_id from draft
  4. INSERT unit_edit_comment (unit_id from draft, not from caller)
  5. {status:'comment_added', comment_id, draft_id, unit_id}

fn_iu_comment(p_address, p_author, p_body, p_kind, p_author_type, p_context) → jsonb

Natural helper. Logic:

  1. Validate
  2. Resolve address → IU
  3. Context-based routing:
    • p_context->>'draft_id' present → validate draft belongs to IU → attach → {status:'comment_added', ..., selected_by:'context_draft_id'}
    • p_context->>'select'='latest' → find latest open draft → attach → {status:'comment_added', ..., selected_by:'latest'}
  4. Auto-routing (no context):
    • 0 open drafts → {status:'no_open_draft', guidance:'To suggest edit: SELECT fn_iu_edit(address, body, actor, reason)', next_action:'fn_iu_edit'}
    • 1 open draft → attach → {status:'comment_added', ..., selected_by:'only_open_draft'}
    • 1 open drafts → {status:'multiple_open_drafts', drafts:[{id, author, created_at, base_version_seq}...], guidance:'Choose draft_id or pass context {select:latest}', next_action:'fn_iu_comment_edit_draft'}

All 4: SECURITY DEFINER, SET search_path=pg_catalog,public, REVOKE ALL FROM PUBLIC, GRANT EXECUTE TO {FN_OWNER}.

Agent: sau CREATE, apply REVOKE + GRANT cho mỗi function.


§3. Tests

# Agent chạy tests sau CREATE thành công.
# Dùng TEST_IU_ADDR captured ở preflight.
# T prefix cho test results.

Test Matrix

# Test Call Expected status
T1 Plan same body fn_iu_edit_plan(TEST_ADDR, SAME_BODY, 'tester') no_change
T2 Plan new body fn_iu_edit_plan(TEST_ADDR, 'new body p3c1', 'tester') plan_ok + hash + policy
T3 Plan bad address fn_iu_edit_plan('nonexistent/addr', 'x', 'tester') iu_not_found
T4 Plan empty body fn_iu_edit_plan(TEST_ADDR, '', 'tester') invalid_input
T5 Create draft fn_iu_create_edit_draft(TEST_ADDR, 'draft body p3c1', 'agent:test', 'test reason') draft_created
T6 Create draft same body fn_iu_create_edit_draft(TEST_ADDR, SAME_BODY, 'agent:test') no_change
T7 Low-level comment fn_iu_comment_edit_draft(T5_DRAFT_ID, 'agent:reviewer', 'looks good', 'review') comment_added
T8 Low-level comment bad draft fn_iu_comment_edit_draft(random_uuid, 'x', 'x') draft_not_found
T9 Natural comment — 1 open draft fn_iu_comment(TEST_ADDR, 'agent:opus', 'I agree') comment_added + selected_by=only_open_draft
T10 Create 2nd draft (different body) fn_iu_create_edit_draft(TEST_ADDR, 'alt body p3c1', 'agent:gemini', 'alt') draft_created
T11 Natural comment — multiple drafts, no context fn_iu_comment(TEST_ADDR, 'agent:opus', 'which one?') multiple_open_drafts + drafts[]
T12 Natural comment — context draft_id fn_iu_comment(TEST_ADDR, 'agent:opus', 'for draft A', 'general', 'agent', '{"draft_id":"T5_ID"}') comment_added + selected_by=context_draft_id
T13 Natural comment — context select=latest fn_iu_comment(TEST_ADDR, 'agent:opus', 'latest one', 'general', 'agent', '{"select":"latest"}') comment_added + selected_by=latest
T14 Natural comment — no open draft (new address) fn_iu_comment('test/p3c1/no-draft-addr', 'agent:x', 'hello') iu_not_found OR no_open_draft
T15 Verify draft count increased count unit_edit_draft DRAFT_BEFORE + 2
T16 Verify comment count increased count unit_edit_comment COMMENT_BEFORE + 4 (T7+T9+T12+T13)
T17 IU/UV counts unchanged compare before/after unchanged
T18 Functions security check REVOKE PUBLIC, DEFINER, search_path all 4 correct
T19 Guidance present in error responses T3/T4/T11 have guidance field non-empty

Mỗi test: ghi status + actual JSON output. PASS/FAIL. Retain pilot drafts/comments.


§4. Report

Include:

  • phase_status, p3c2_readiness
  • 4 functions created (signatures)
  • Security: DEFINER, search_path, owner, grants
  • Test matrix T1–T19
  • UV lifecycle convention
  • Counts before→after (IU/UV/draft/comment)
  • Gateway intact
  • UX evidence: sample JSON for plan_ok, draft_created, comment_added, no_open_draft, multiple_open_drafts, invalid_input
  • Pilot draft_ids + addresses retained
  • Boundaries honored

Hard Boundaries

  • ❌ No table DDL / No trigger / No gateway changes
  • ❌ No IU/UV writes (P3C1 functions don't write IU/UV)
  • ❌ No fn_iu_apply_edit_draft / fn_iu_edit (P3C2)
  • ❌ No vector / No cleanup / No retry

Opus Design Decisions

#1 fn_iu_comment multi-draft behavior — revised. GPT đúng: "đơn giản mà nhầm không phải thiết kế tốt." 1 draft = auto, >1 = stop + candidates. Context cho advanced AI: {draft_id:...} hoặc {select:'latest'}.

#2 Split P3C → P3C1 + P3C2. P3C1 = 4 safe functions (no IU/UV writes). P3C2 = apply + wrapper (write path). Apply là phần nguy hiểm nhất — tách test riêng.


23-P3C1 Prompt | 2026-05-07 | 4 safe functions | Natural comment | Chờ GPT/User review