KB-C691

Điều 24: Luật Nhãn — Label Law v1.3 (6 rounds GPT+Gemini reviewed)

13 min read Revision 1
lawdieu-24

ĐIỀU 24: LUẬT NHÃN — LABEL LAW v1.3

(Bổ sung Hiến pháp Kiến trúc — Quản lý phân loại đa chiều cho toàn hệ thống)

LUẬT v1.3 CHÍNH THỨC | S122 (2026-03-15) | ĐỒNG THUẬN: Huyen + Claude + GPT (4 rounds) + Gemini (4 rounds). ĐÓNG BĂNG. v1.3 thay đổi từ v1.2: +Khóa NULL bypass composite FK (GPT R3🔴). +Trigger DDL per-table theo cột thực có (GPT R3🔴). +BEFORE INSERT auto-fill parent_facet (Gemini R3🟡). +Max 3 nhãn domain/entity chống label farming (Gemini R3🟡). +"Cùng nhóm" giữ per-facet domain (GPT R3 vs Gemini R3 — Claude quyết: per-facet đúng hơn). Đọc: search_knowledge("luật nhãn label law taxonomy")


I–IV. (Giữ nguyên từ v1.2)

Vấn đề (7 hệ thống rời rạc) → Nguyên tắc (Faceted, SSOT, Tự động) → Ma trận 6×6 → 6 chiều chi tiết.


V. CẤU TRÚC PG + DIRECTUS — ASSEMBLY FIRST

5.1 Schema — 5 collections (v1.3)

-- 1. CHIỀU PHÂN LOẠI (6 records)
CREATE TABLE taxonomy_facets (
  id          serial PRIMARY KEY,
  code        text UNIQUE NOT NULL,
  name        text NOT NULL,
  description text,
  cardinality text NOT NULL DEFAULT 'single'
    CHECK (cardinality IN ('single','multiple')),
  max_labels_per_entity smallint DEFAULT 0,  -- v1.3 Gemini R3: 0=unlimited, >0=giới hạn
  sort        integer DEFAULT 0,
  status      text DEFAULT 'active'
);

-- 2. GIÁ TRỊ NHÃN (phân cấp 3 tầng)
CREATE TABLE taxonomy (
  id           serial PRIMARY KEY,
  code         text UNIQUE NOT NULL,
  name         text NOT NULL,
  name_en      text,
  facet_id     integer NOT NULL REFERENCES taxonomy_facets(id),
  parent_id    integer,
  parent_facet integer,
  depth        smallint NOT NULL DEFAULT 0
    CHECK (depth >= 0 AND depth <= 2),
  description  text,
  scope        text[],
  status       text DEFAULT 'active'
    CHECK (status IN ('active','deprecated')),
  replaced_by  text,
  sort         integer DEFAULT 0,
  date_created timestamp DEFAULT now(),

  -- TREE CONSTRAINTS
  CONSTRAINT chk_root_no_parent CHECK (
    (depth = 0 AND parent_id IS NULL AND parent_facet IS NULL)
    OR (depth > 0 AND parent_id IS NOT NULL AND parent_facet IS NOT NULL)
  ),
  -- ^^^ v1.3 GPT R3🔴: Khóa NULL bypass — root: BOTH NULL, non-root: BOTH NOT NULL

  -- Composite FK same-facet (thuần PG, không trigger)
  CONSTRAINT uq_taxonomy_id_facet UNIQUE (id, facet_id),
  CONSTRAINT fk_taxonomy_parent_same_facet
    FOREIGN KEY (parent_id, parent_facet) REFERENCES taxonomy(id, facet_id),
  CONSTRAINT chk_parent_facet_match CHECK (parent_facet = facet_id),

  -- replaced_by constraints
  CONSTRAINT chk_replaced_by_not_self CHECK (replaced_by != code),
  CONSTRAINT fk_replaced_by FOREIGN KEY (replaced_by) REFERENCES taxonomy(code)
);

-- v1.3 Gemini R3🟡: Directus không biết composite FK → auto-fill parent_facet
CREATE OR REPLACE FUNCTION fn_taxonomy_auto_fill_parent_facet()
RETURNS trigger AS $$
BEGIN
  IF NEW.parent_id IS NOT NULL THEN
    NEW.parent_facet := NEW.facet_id;
  ELSE
    NEW.parent_facet := NULL;
  END IF;
  RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_taxonomy_fill_parent_facet
  BEFORE INSERT OR UPDATE ON taxonomy FOR EACH ROW
  EXECUTE FUNCTION fn_taxonomy_auto_fill_parent_facet();

-- Chống cycle thật (giữ nguyên v1.2)
CREATE OR REPLACE FUNCTION chk_no_cycle() RETURNS trigger AS $$
DECLARE v_current integer; v_depth integer := 0;
BEGIN
  IF NEW.parent_id IS NULL THEN RETURN NEW; END IF;
  v_current := NEW.parent_id;
  WHILE v_current IS NOT NULL AND v_depth < 5 LOOP
    IF v_current = NEW.id THEN
      RAISE EXCEPTION 'Cycle detected in taxonomy tree';
    END IF;
    SELECT parent_id INTO v_current FROM taxonomy WHERE id = v_current;
    v_depth := v_depth + 1;
  END LOOP;
  RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_taxonomy_no_cycle
  BEFORE INSERT OR UPDATE ON taxonomy FOR EACH ROW
  EXECUTE FUNCTION chk_no_cycle();

-- replaced_by validation (giữ nguyên v1.2)
CREATE OR REPLACE FUNCTION chk_replaced_by_valid() RETURNS trigger AS $$
DECLARE v_target_facet integer; v_chain text; v_depth integer := 0;
BEGIN
  IF NEW.replaced_by IS NULL THEN RETURN NEW; END IF;
  SELECT facet_id INTO v_target_facet FROM taxonomy WHERE code = NEW.replaced_by;
  IF v_target_facet != NEW.facet_id THEN
    RAISE EXCEPTION 'replaced_by must point to same facet';
  END IF;
  v_chain := NEW.replaced_by;
  WHILE v_chain IS NOT NULL AND v_depth < 5 LOOP
    IF v_chain = NEW.code THEN RAISE EXCEPTION 'Replacement cycle detected'; END IF;
    SELECT replaced_by INTO v_chain FROM taxonomy WHERE code = v_chain;
    v_depth := v_depth + 1;
  END LOOP;
  RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_taxonomy_replaced_by
  BEFORE INSERT OR UPDATE ON taxonomy FOR EACH ROW
  EXECUTE FUNCTION chk_replaced_by_valid();

-- Indexes
CREATE INDEX idx_taxonomy_facet_parent_sort ON taxonomy(facet_id, parent_id, sort);
CREATE INDEX idx_taxonomy_depth ON taxonomy(depth);

-- 3. MA TRẬN CHIỀU × LỚP (36 records)
CREATE TABLE taxonomy_matrix (
  id                serial PRIMARY KEY,
  facet_id          integer NOT NULL REFERENCES taxonomy_facets(id),
  composition_level text NOT NULL,
  requirement       text NOT NULL DEFAULT 'not_applicable'
    CHECK (requirement IN ('mandatory','optional','not_applicable')),
  UNIQUE(facet_id, composition_level)
);

-- 4. GÁN NHÃN CHO ENTITY (junction)
CREATE TABLE entity_labels (
  id           serial PRIMARY KEY,
  entity_code  text NOT NULL,
  label_code   text NOT NULL REFERENCES taxonomy(code),
  assigned_by  text DEFAULT 'auto'
    CHECK (assigned_by IN ('auto','rule','user','ai','dot')),
  rule_id      integer,
  assigned_at  timestamp DEFAULT now(),
  UNIQUE(entity_code, label_code)
);
CREATE INDEX idx_el_entity ON entity_labels(entity_code);
CREATE INDEX idx_el_label ON entity_labels(label_code);

-- v_all_entity_codes (giữ nguyên v1.2)
CREATE OR REPLACE VIEW v_all_entity_codes AS
  SELECT code FROM dot_tools WHERE code IS NOT NULL
  UNION ALL SELECT code FROM workflows WHERE code IS NOT NULL
  UNION ALL SELECT code FROM checkpoint_types WHERE code IS NOT NULL
  UNION ALL SELECT code FROM modules WHERE code IS NOT NULL
  UNION ALL SELECT code FROM tasks WHERE code IS NOT NULL
  UNION ALL SELECT code FROM agents WHERE code IS NOT NULL
  UNION ALL SELECT code FROM ui_pages WHERE code IS NOT NULL
  UNION ALL SELECT code FROM collection_registry WHERE code IS NOT NULL
  UNION ALL SELECT code FROM table_registry WHERE code IS NOT NULL
  UNION ALL SELECT code FROM entity_dependencies WHERE code IS NOT NULL
  UNION ALL SELECT code FROM workflow_steps WHERE code IS NOT NULL
  UNION ALL SELECT code FROM checkpoint_sets WHERE code IS NOT NULL
  UNION ALL SELECT code FROM workflow_change_requests WHERE code IS NOT NULL
  UNION ALL SELECT code FROM system_issues WHERE code IS NOT NULL
  UNION ALL SELECT code FROM meta_catalog WHERE code IS NOT NULL
  UNION ALL SELECT code FROM taxonomy WHERE code IS NOT NULL;

-- 5. QUY TẮC GÁN TỰ ĐỘNG
CREATE TABLE label_rules (
  id              serial PRIMARY KEY,
  name            text NOT NULL,
  facet_id        integer NOT NULL REFERENCES taxonomy_facets(id),
  rule_type       text NOT NULL
    CHECK (rule_type IN ('collection','keyword','relation')),
  condition       jsonb NOT NULL
    CHECK (jsonb_typeof(condition) = 'object')
    CHECK (condition ? 'collection' OR condition ? 'name_pattern' OR condition ? 'relation'),
  result_label    text NOT NULL REFERENCES taxonomy(code),
  priority        integer DEFAULT 100,
  skip_wide_warning boolean DEFAULT false,
  status          text DEFAULT 'active'
);

5.2 Conflict Resolution (giữ nguyên v1.2 + bổ sung v1.3)

(Giữ nguyên bảng conflict v1.2)

v1.3 Gemini R3: Chống label farming. Chiều NHIỀU có max_labels_per_entity trong taxonomy_facets (ví dụ: domain=3). Nếu auto-assign vượt threshold → KHÔNG gán thêm, ghi system_issues "entity X đã có 3 nhãn domain, rule R20 muốn gán thêm — cần review bộ keyword".

5.3 PG Function gán tự động (v1.3 — per-table DDL)

-- Function chung (giữ nguyên v1.2 data-driven to_jsonb)
CREATE OR REPLACE FUNCTION fn_auto_label_assignment()
RETURNS trigger AS $$
DECLARE
  v_entity_code text;
  v_collection text;
  v_row jsonb;
  v_name text;
  v_desc text;
  v_facet_max smallint;
  v_current_count integer;
  r label_rules%ROWTYPE;
BEGIN
  IF TG_OP = 'UPDATE' THEN
    IF (to_jsonb(NEW)->>'name') IS NOT DISTINCT FROM (to_jsonb(OLD)->>'name')
       AND (to_jsonb(NEW)->>'description') IS NOT DISTINCT FROM (to_jsonb(OLD)->>'description') THEN
      RETURN NEW;
    END IF;
  END IF;

  v_collection := TG_TABLE_NAME;
  v_row := to_jsonb(NEW);
  v_entity_code := v_row->>'code';
  IF v_entity_code IS NULL THEN RETURN NEW; END IF;

  v_name := COALESCE(v_row->>'name', '');
  v_desc := COALESCE(v_row->>'description', '');

  FOR r IN
    SELECT * FROM label_rules WHERE status = 'active' ORDER BY priority ASC
  LOOP
    -- v1.3 Gemini R3: check max_labels_per_entity trước khi gán
    SELECT max_labels_per_entity INTO v_facet_max
    FROM taxonomy_facets WHERE id = r.facet_id;
    IF v_facet_max > 0 THEN
      SELECT COUNT(*) INTO v_current_count
      FROM entity_labels el JOIN taxonomy t ON el.label_code = t.code
      WHERE el.entity_code = v_entity_code AND t.facet_id = r.facet_id;
      IF v_current_count >= v_facet_max THEN
        CONTINUE;  -- skip, đã đạt giới hạn
      END IF;
    END IF;

    IF r.rule_type = 'collection'
       AND r.condition->>'collection' = v_collection THEN
      INSERT INTO entity_labels(entity_code, label_code, assigned_by, rule_id)
      VALUES (v_entity_code, r.result_label, 'rule', r.id)
      ON CONFLICT (entity_code, label_code) DO NOTHING;
    END IF;

    IF r.rule_type = 'keyword'
       AND (v_name ~* (r.condition->>'name_pattern')
            OR v_desc ~* (r.condition->>'name_pattern')) THEN
      INSERT INTO entity_labels(entity_code, label_code, assigned_by, rule_id)
      VALUES (v_entity_code, r.result_label, 'rule', r.id)
      ON CONFLICT (entity_code, label_code) DO NOTHING;
    END IF;
  END LOOP;

  RETURN NEW;
END; $$ LANGUAGE plpgsql;

v1.3 GPT R3🔴: Trigger DDL per-table theo cột thực có.

Không dùng 1 template cứng UPDATE OF name, description cho mọi table. Phải generate theo cột thực:

-- Table có CẢ name + description (đa số: dot_tools, workflows, modules...)
CREATE TRIGGER trg_label_assign_dot_tools
  AFTER INSERT OR UPDATE OF name, description ON dot_tools
  FOR EACH ROW EXECUTE FUNCTION fn_auto_label_assignment();

-- Table CHỈ CÓ name, KHÔNG có description (ví dụ: agents)
CREATE TRIGGER trg_label_assign_agents
  AFTER INSERT OR UPDATE OF name ON agents
  FOR EACH ROW EXECUTE FUNCTION fn_auto_label_assignment();

-- Table KHÔNG CÓ name (ví dụ: entity_dependencies — chỉ có code)
CREATE TRIGGER trg_label_assign_deps
  AFTER INSERT ON entity_dependencies
  FOR EACH ROW EXECUTE FUNCTION fn_auto_label_assignment();

Quy tắc generate trigger:

  • Kiểm tra information_schema.columns để biết table có field nào
  • Có name + description → AFTER INSERT OR UPDATE OF name, description
  • Có name only → AFTER INSERT OR UPDATE OF name
  • Không có name → AFTER INSERT only (chỉ collection rules, không keyword)
  • DOT tool dot-label-trigger-setup sẽ tự generate DDL đúng per table

5.4 "Cùng nhóm" = Scored Union Top N — PER-FACET (v1.3)

Giữ per-facet domain (GPT R3 đồng ý, Claude quyết). Cross-facet scoring = ý nghĩa "Tương tự" (heading 6), không phải "Cùng nhóm" (heading 5). Mở rộng cross-facet ở phase 2.

-- Giữ nguyên v1.2 — filter facet_id = domain
SELECT el2.entity_code,
       COUNT(*) AS shared_labels,
       array_agg(t.name) AS shared_label_names
FROM entity_labels el1
JOIN entity_labels el2 ON el1.label_code = el2.label_code
JOIN taxonomy t ON el1.label_code = t.code
WHERE el1.entity_code = :source_entity
  AND el2.entity_code != :source_entity
  AND t.facet_id = (SELECT id FROM taxonomy_facets WHERE code = 'FAC-01')
GROUP BY el2.entity_code
ORDER BY shared_labels DESC
LIMIT 20;

5.5 Directus (giữ nguyên v1.2)

AI Agent READ-only. Tắt Count junction. parent_facet ẩn trong Directus (hidden field), auto-fill bằng BEFORE INSERT trigger → Directus chỉ thấy parent_id M2O, Tree View hoạt động bình thường.


VI–IX. (Giữ nguyên từ v1.2)

Quy tắc gán 3 tầng + Registries UI 6×6 + DOT kiểm tra + Migration.

v1.3 bổ sung DOT: dot-label-trigger-setup — generate DDL trigger đúng per table dựa trên information_schema.columns.


Điều 24 v1.3 | S122 (2026-03-15) | Huyen + Claude + GPT R1+R2+R3 + Gemini R1+R2+R3. 6 rounds review. NULL bypass sealed. Per-table trigger DDL. Label farming threshold. parent_facet auto-fill.