KB-BDE6

unit_kind vocab governance — dot_config prefix vocab.unit_kind.* (2026-05-27)

5 min read Revision 1
iu-cutunit-kindvocabdot-configgovernance2026-05-27

03 — unit_kind Vocab Governance

Live source of truth

The accepted set of unit_kind values at CUT time is defined data-side in dot_config rows whose key matches the prefix vocab.unit_kind.. There is no companion tac_unit_kind_vocab FK table.

Snapshot at 2026-05-27 (post-fix)

SELECT key, value, description FROM dot_config WHERE key LIKE 'vocab.unit_kind.%' ORDER BY key;
key value description
vocab.unit_kind.design_doc_section design_doc_section Section trong design document — proposed_pilot
vocab.unit_kind.law_unit law_unit p3d-phase4-vocab-20260511-082717

Total: 2 entries. Snapshot UNCHANGED by this macro (vocab not widened).

Code path that consults this vocab

public.fn_iu_create
  |- v_uk := public.fn_iu_resolve_default(
              p_unit_kind,
              'iu_create.default_unit_kind',
              'vocab.unit_kind.'        -- prefix lookup in dot_config
            );
     IF v_uk->>'status' NOT IN ('explicit','default','auto_single')
        THEN RAISE EXCEPTION 'unit_kind: %', v_uk->>'message';

So fn_iu_create will accept exactly the values present in vocab.unit_kind.<value> keys, plus an iu_create.default_unit_kind fallback when input is NULL.

Code path that consults this vocab (after fix)

public.fn_cut_mark_staged_file (cut_manifest_piece_schema_v1, NEW)
  |- FOR each piece:
       PERFORM 1 FROM public.dot_config
         WHERE key = 'vocab.unit_kind.' || v_piece->>'unit_kind';
       IF NOT FOUND THEN RAISE EXCEPTION ... ;

public.fn_iu_verify_mark (NEW Axis D)
  |- SELECT count(*) ... WHERE EXISTS (
       SELECT 1 FROM public.dot_config
       WHERE key = 'vocab.unit_kind.' || (p->>'unit_kind')
     );

All three sites (fn_iu_create, fn_cut_mark_staged_file, fn_iu_verify_mark) now consult the same dot_config prefix — a single source of truth. Adding a new unit_kind value is a single INSERT into dot_config and instantly propagates to MARK gate, VERIFY_MARK Axis D, and CUT.

Governance pattern parity

This matches the proven pattern for section_type:

Vocab dot_config prefix tac_ FK table Validated at MARK gate Validated at VERIFY_MARK
section_type vocab.section_type. (18 keys) tac_section_type_vocab (17 keys) yes (pre-existing) Axis B (non-empty), now also piece-level dot_config lookup at MARK
unit_kind vocab.unit_kind. (2 keys) none NEW NEW Axis D

Note the asymmetry: section_type also has a tac_ FK table that backstops information_unit.section_type; unit_kind is enforced ONLY by dot_config plus fn-body checks (no FK on information_unit.unit_kind). This is fragile and is flagged as carry-forward CF-3 in section 07.

When to add a new unit_kind

If a new document kind needs its own unit_kind value (e.g. runbook_step, policy_clause):

  1. Get governance approval (a Điều/runbook entry explicitly enumerating the value).
  2. Execute:
    INSERT INTO dot_config(key, value, description, updated_at)  VALUES ('vocab.unit_kind.<value>', '<value>', '<governance ref>', now());
    
  3. NO function body change required.
  4. MARK callers can now emit unit_kind=<value> immediately.

Why semantic labels are not first-class here

law_section, design_doc_subsection, policy_step, etc. are semantic labels meaningful to authors but coarser than runtime unit_kind. Two design options exist for handling them:

Option Storage Trade-off
Strict (chosen) Caller emits runtime value (law_unit). Manifest stores law_unit. Simple. Loses semantic distinction in the manifest. Forces caller-side normalization.
Semantic mapping (rejected) Caller emits law_section. fn_cut_mark_staged_file resolves via dot_config mapping.unit_kind.<sem> = <runtime>, rewrites piece, stores runtime value. Centralizes mapping; permits semantic labels in caller code. Lossy at storage time. Adds a second config namespace. Operator rejected.

If a future macro reintroduces semantic mapping, the recommended placement is:

  • dot_config mapping.unit_kind.<sem> = <runtime> (data-side)
  • New helper fn_cut_resolve_unit_kind(text) RETURNS text
  • fn_cut_mark_staged_file rewrites p_pieces before calling fn_iu_op_mark_file
  • Axis D refuses unresolvable values only

But that is explicitly out of scope for this macro.

Conclusion

unit_kind runtime vocab governance is now consistently enforced at three sites (fn_iu_create, fn_cut_mark_staged_file, fn_iu_verify_mark) reading from one dot_config prefix. Vocab additions remain data-side (single INSERT, no DDL).

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-cut-dieu39-unit-kind-root-cause-and-contract-fix/03-unit-kind-vocab-governance.md