23-P3C1 — Edit Draft Safe Functions — Execution Prompt
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:
- Validate non-empty: address, body, actor →
{status:'invalid_input', field, guidance} - Resolve address → IU →
iu_not_foundnếu missing - HEAD UV →
no_head_versionnếu NULL - Hash via fn_content_hash(p_body) →
no_changenếu trùng - Count open drafts cho unit_id
- Read policy
- →
{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:
- Validate
- Resolve address → IU + HEAD UV
- canonical_address = IU.canonical_address (from DB, not p_address)
- Hash, no-change check
- INSERT unit_edit_draft
- →
{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:
- Validate (including kind IN allow-list, author_type IN allow-list)
- Load draft →
draft_not_found - Resolve unit_id from draft
- INSERT unit_edit_comment (unit_id from draft, not from caller)
- →
{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:
- Validate
- Resolve address → IU
- 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'}
- 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'}
- 0 open drafts →
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