KB-5808

dot-iu-cutter v0.2 — Phase α fn_tac_birth_gate_lu Read-Only Inspection (2026-05-15)

14 min read Revision 1
dieu44-trien-khaidot-iu-cutterv0.2phase-alphafn-tac-birth-gate-luread-only-inspectionb-ddl-32026-05-15

dot-iu-cutter v0.2 — Phase α fn_tac_birth_gate_lu Read-Only Inspection

document_path: knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-fn-tac-birth-gate-lu-inspection-2026-05-15.md
revision: r1
date: 2026-05-15
author: Agent (Claude Code CLI, Opus 4.7 1M)
phase: v0.2 — Phase α DDL authoring prerequisite (B-DDL-3)
mutation_performed: false
function_modified: false

§1 — Purpose

Resolve B-DDL-3 from the Phase α design report: read-only inspection of the body of public.fn_tac_birth_gate_lu (the BEFORE INSERT OR UPDATE trigger function on public.tac_logical_unit) to confirm that adding the new Phase α columns (authority, canonical_address_format_version) does not conflict with any implicit constraint embedded in the trigger function.

Inspection method: pg_get_functiondef() + pg_get_triggerdef() via psql over SSH; read-only.


§2 — Function Signature

schema:         public
name:           fn_tac_birth_gate_lu
arguments:      (none)
returns:        trigger
language:       plpgsql
security:       SECURITY DEFINER
search_path:    'public', 'pg_catalog'
volatility:     v (volatile)

§3 — Trigger Definition

CREATE TRIGGER trg_tac_birth_gate_lu
BEFORE INSERT OR UPDATE ON tac_logical_unit
FOR EACH ROW EXECUTE FUNCTION fn_tac_birth_gate_lu();

Fires on every row-level INSERT or UPDATE on public.tac_logical_unit. Phase α DDL is ADD COLUMN (metadata-only; does NOT fire INSERT/UPDATE triggers); the backfill is an UPDATE (does fire the trigger per row).


§4 — Function Body (verbatim)

CREATE OR REPLACE FUNCTION public.fn_tac_birth_gate_lu()
 RETURNS trigger
 LANGUAGE plpgsql
 SECURITY DEFINER
 SET search_path TO 'public', 'pg_catalog'
AS $function$
DECLARE
    v_section_active BOOLEAN;
    v_parent_doc_code TEXT;
BEGIN
    IF NEW.canonical_address !~ '^D38-[A-Z0-9]+-((ROOT)|(S[0-9]+(-P[0-9]+(-[0-9]+)*)?))$' THEN
        RAISE EXCEPTION 'BG-LU-01: canonical_address % không khớp regex D38-local', NEW.canonical_address
            USING ERRCODE = 'check_violation';
    END IF;

    IF NEW.doc_code IS NULL OR pg_catalog.length(pg_catalog.btrim(NEW.doc_code)) = 0 THEN
        RAISE EXCEPTION 'BG-LU-02: doc_code rỗng' USING ERRCODE = 'not_null_violation';
    END IF;

    IF NEW.parent_id IS NOT NULL THEN
        SELECT doc_code INTO v_parent_doc_code
          FROM public.tac_logical_unit
         WHERE id = NEW.parent_id;
        IF v_parent_doc_code IS NULL THEN
            RAISE EXCEPTION 'BG-LU-03: parent_id % không tồn tại', NEW.parent_id
                USING ERRCODE = 'foreign_key_violation';
        END IF;
        IF v_parent_doc_code != NEW.doc_code THEN
            RAISE EXCEPTION 'BG-LU-03: parent doc_code % != child doc_code %',
                v_parent_doc_code, NEW.doc_code
                USING ERRCODE = 'check_violation';
        END IF;
    END IF;

    SELECT (lifecycle_status = 'active') INTO v_section_active
      FROM public.tac_section_type_vocab
     WHERE code = NEW.section_type;
    IF NOT coalesce(v_section_active, FALSE) THEN
        RAISE EXCEPTION 'BG-LU-04: section_type % không active', NEW.section_type
            USING ERRCODE = 'check_violation';
    END IF;

    IF NEW.owner IS NULL OR pg_catalog.length(pg_catalog.btrim(NEW.owner)) = 0 THEN
        RAISE EXCEPTION 'BG-LU-05: owner rỗng' USING ERRCODE = 'not_null_violation';
    END IF;

    IF NEW.sort_order IS NULL OR NEW.sort_order < 0 THEN
        RAISE EXCEPTION 'BG-LU-06: sort_order invalid' USING ERRCODE = 'check_violation';
    END IF;

    NEW.updated_at := now();
    RETURN NEW;
END;
$function$

§5 — Reference Inventory (what the body uses)

columns_referenced (NEW.*):
  - NEW.canonical_address     (read; regex-validated)
  - NEW.doc_code              (read; non-empty check)
  - NEW.parent_id             (read; existence + same-doc check)
  - NEW.section_type          (read; FK + vocab-active check)
  - NEW.owner                 (read; non-empty check)
  - NEW.sort_order            (read; >= 0 check)
  - NEW.updated_at            (WRITTEN to now() — side effect)

tables_referenced:
  - public.tac_logical_unit          (SELECT to resolve parent_id → doc_code)
  - public.tac_section_type_vocab    (SELECT to confirm section_type is lifecycle 'active')
  (note: `lifecycle_status='active'` here refers to tac_section_type_vocab.lifecycle_status, NOT tac_logical_unit.lifecycle_status)

vocabulary_refs:
  - regex hardcoded: ^D38-[A-Z0-9]+-((ROOT)|(S[0-9]+(-P[0-9]+(-[0-9]+)*)?))$

§6 — Does the Function Reference canonical_address?

yes_references_NEW.canonical_address: TRUE
nature_of_reference: regex validation only (no mutation)
regex_pattern: ^D38-[A-Z0-9]+-((ROOT)|(S[0-9]+(-P[0-9]+(-[0-9]+)*)?))$
exception_code_on_mismatch: BG-LU-01 (check_violation)

implication_for_phase_α: NONE
  - Phase α does NOT modify canonical_address
  - Phase α backfill UPDATE sets `authority` only; canonical_address remains unchanged
  - the regex check re-runs during backfill UPDATE for each row; it will pass because the existing value already conforms (all 86 production rows were inserted under this trigger)

§7 — Does the Function Reference lifecycle_status?

yes_references_lifecycle_status: TRUE
but_it_is_NOT_NEW.lifecycle_status:
  - the lifecycle_status read inside the function is tac_section_type_vocab.lifecycle_status
  - NOT public.tac_logical_unit.lifecycle_status
  - these are DIFFERENT lifecycle vocabularies for different concerns

implication_for_phase_α_and_BR-4: NONE
  - BR-4 mapping reads tac_logical_unit.lifecycle_status to derive authority
  - the trigger reads tac_section_type_vocab.lifecycle_status to validate section_type
  - the two never collide

§8 — Does the Function Reference authority or canonical_address_format_version?

authority_referenced: FALSE
canonical_address_format_version_referenced: FALSE

implication_for_phase_α: SAFE
  - Phase α can add both columns without the trigger seeing them
  - the trigger does NOT validate, write, or block on either new column
  - the trigger fires on the backfill UPDATE but is INDIFFERENT to the new columns

§9 — Conflict Assessment for Phase α

9.1 ADD COLUMN operations

operation: ALTER TABLE public.tac_logical_unit ADD COLUMN authority text NULL DEFAULT 'draft'
trigger_fires? NO
  - ADD COLUMN is metadata DDL; does not fire row-level INSERT/UPDATE triggers
conflict? NONE

operation: ALTER TABLE public.tac_logical_unit ADD COLUMN canonical_address_format_version text NOT NULL DEFAULT 'canonical-address-v1'
trigger_fires? NO
conflict? NONE

9.2 Backfill UPDATE

operation: UPDATE public.tac_logical_unit SET authority = CASE lifecycle_status WHEN 'draft_only' THEN 'draft' WHEN 'active' THEN 'enacted' WHEN 'retired' THEN 'enacted' END WHERE authority IS NULL;
trigger_fires? YES — BEFORE UPDATE FOR EACH ROW, 86 times
per_row_trigger_behavior:
  - canonical_address regex check: PASSES (canonical_address unchanged; already conforms)
  - doc_code non-empty: PASSES (existing value)
  - parent_id existence + same-doc check: PASSES (existing parents are valid)
  - section_type vocab check: PASSES (vocab is stable; all 86 rows have valid section_type)
  - owner non-empty: PASSES (existing value)
  - sort_order >= 0: PASSES (existing sort_order; CHECK constraint already in place)
  - NEW.updated_at := now(): EXECUTED — every row's updated_at gets bumped to backfill time

side_effect_to_flag:
  updated_at_bump_on_all_86_rows: TRUE
  consequence: original `updated_at` timestamps will be lost on backfilled rows
  acceptance: this is normal UPDATE semantics; the bump itself is correct behavior; no escalation required
  alternative_(NOT_recommended): SET LOCAL session_replication_role='replica' to bypass triggers during backfill — would skip validation; NOT worth the risk

conflict? NONE (with the documented side effect)

9.3 New table creation (cutter_governance.canonical_address_alias)

operation: CREATE TABLE IF NOT EXISTS cutter_governance.canonical_address_alias (...)
trigger_fires? NO
  - DDL on a different schema; trg_tac_birth_gate_lu only fires on public.tac_logical_unit
conflict? NONE

9.4 Sandbox mirror

operation: ALTER TABLE sandbox_tac.logical_unit ADD COLUMN ... (no backfill)
trigger_fires? NO
  - sandbox_tac has its own helper function (sandbox_tac.fn_sbx_lu_parent_same_doc) but inspection earlier confirmed it does NOT reference the new columns either
conflict? NONE

§10 — Observations Beyond Conflict (informational)

O-1: trigger regex hardcodes "D38-" prefix
  current_implication: only D38-* canonical_addresses validate; D40-*, D41-* etc. would be rejected at INSERT/UPDATE
  Phase_α_implication: NONE (all 86 existing rows are D38-prefixed; Phase α adds no rows)
  future_implication: when other doc ids land, the trigger regex must be revised — this is a Đ24/Đ32 followup item for v0.3+ (NOT a v0.2 blocker)

O-2: trigger regex grammar matches BR-5 canonical-address-v1 grammar exactly
  effect: write-time enforcement of canonical-address-v1 is ALREADY active in production
  this is positive corroboration of the BR-5 ratification choice — the syntax was already enforced; BR-5 simply documented the implicit rule

O-3: trigger reads tac_section_type_vocab to enforce section_type FK + activity
  effect: rows with section_type values that have been DEACTIVATED in the vocab would fail re-validation during backfill
  current_state: 86 rows × {heading, paragraph, checklist, principle, technical_spec, governance_process, process} section_types — all active in vocab
  mitigation: pre-backfill, verify no section_type in current 86 rows is inactive in the vocab; if any inactive value found, address before backfill

O-4: trigger uses SECURITY DEFINER + search_path 'public', 'pg_catalog'
  effect: function runs with definer's privileges (typically superuser at create time); search_path prevents schema poisoning
  Phase_α_implication: NONE; the function does not write to anything outside its trigger scope

§11 — Risk Assessment

risk_to_phase_α_from_fn_tac_birth_gate_lu: NONE
risk_per_operation:
  ADD COLUMN authority:                     0 (trigger does not fire)
  ADD COLUMN format_version:                0 (trigger does not fire)
  UPDATE backfill of authority:             0 (trigger fires but is indifferent to authority; canonical_address regex passes)
  CREATE TABLE alias:                       0 (different schema; trigger does not fire)
  ALTER TABLE sandbox_tac mirror:            0 (different schema; trigger does not fire)

documented_side_effect (not a risk):
  updated_at bumped on all 86 rows during backfill (normal UPDATE semantics)

dependencies_to_validate_before_backfill_(simple_queries; not blocking):
  Q-CHK-1: every existing tac_logical_unit row's section_type is currently active in tac_section_type_vocab
  Q-CHK-2: every existing tac_logical_unit row's parent_id (when non-null) still resolves AND shares doc_code
  Q-CHK-3: every existing canonical_address still matches the regex (defensive; should already be true since trigger has been live)
  expected_outcome: all 3 queries return zero failing rows
  authoring_location: include these checks in the Phase α dry-run preflight scenario set

§12 — Recommendation

recommendation_for_phase_α: PROCEED with Phase α DDL authoring as designed

specifics:
  - no DDL modification required to accommodate fn_tac_birth_gate_lu
  - no trigger modification required
  - backfill UPDATE is safe; updated_at side effect is acceptable
  - include Q-CHK-1/2/3 (§11) as pre-backfill verification queries in the dry-run plan
  - leave the regex's hardcoded D38- prefix as a v0.3+ followup item; not a Phase α blocker

§13 — Hard Boundaries

no_function_modified: TRUE
no_DDL_executed: TRUE
no_mutation: TRUE
no_function_replaced: TRUE
no_trigger_recreated: TRUE
read_only_inspection_only: TRUE
output_form: br_phase_α_fn_inspection_read_only

§14 — Cross-References

phase_α_design_master: knowledge/dev/laws/dieu44-trien-khai/v0.2-design/dot-iu-cutter-v0.2-phase-alpha-design-master-2026-05-15.md  (B-DDL-3 prerequisite cited here)
schema_design:        knowledge/dev/laws/dieu44-trien-khai/v0.2-design/dot-iu-cutter-v0.2-phase-alpha-canonical-address-schema-design-2026-05-15.md
risk_review_plan:     knowledge/dev/laws/dieu44-trien-khai/v0.2-design/dot-iu-cutter-v0.2-phase-alpha-risk-review-plan-2026-05-15.md  (RS-5/RS-6 surfaces this inspection)
ddl_draft_companion:  knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-ddl-draft-2026-05-15.sql.md
ddl_verification_plan: knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-ddl-verification-plan-2026-05-15.md
ddl_authoring_report:  knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-ddl-authoring-report-2026-05-15.md
br_5_closure_(grammar_match): knowledge/dev/laws/dieu44-trien-khai/v0.2-planning/dot-iu-cutter-v0.2-br-5-canonical-address-v1-ratification-closure-2026-05-15.md
br_3_reader_writer_inventory: knowledge/dev/laws/dieu44-trien-khai/v0.2-planning/dot-iu-cutter-v0.2-br-3-canonical-address-reader-writer-inventory-2026-05-15.md

End of fn_tac_birth_gate_lu read-only inspection.

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-fn-tac-birth-gate-lu-inspection-2026-05-15.md