KB-3C85

Contract Fix — cut_manifest_piece_schema_v1 7-field + VERIFY_MARK Axis D (2026-05-27)

7 min read Revision 1
iu-cutcontract-fixcut-manifest-piece-schema-v1verify-mark-axis-dfn-cut-mark-staged-filefn-iu-verify-mark2026-05-27

04 — Contract Fix (mig: dieu39_unit_kind_fix)

Apply mode

Single transaction, COMMITTED in one psql script. 2 x CREATE OR REPLACE FUNCTION.

BEGIN
CREATE FUNCTION  -- fn_cut_mark_staged_file
CREATE FUNCTION  -- fn_iu_verify_mark
COMMIT

Script md5 = ef6b9e52337f1e90aea900358225dd83 (file /tmp/dieu39_unit_kind_fix.sql, 313 lines).

Diff at a glance

fn_cut_mark_staged_file — cut_manifest_piece_schema_v1 widened 6 to 7 fields

-- NEW block inserted into the per-piece validation FOR loop:

  -- NEW: unit_kind required and must be in live dot_config vocab.unit_kind.*
  IF v_uk_val IS NULL OR btrim(v_uk_val) = '' THEN
    RAISE EXCEPTION 'fn_cut_mark_staged_file: piece[%].unit_kind is required (cut_manifest_piece_schema_v1)', v_idx;
  END IF;
  PERFORM 1 FROM public.dot_config WHERE key = 'vocab.unit_kind.' || v_uk_val;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cut_mark_staged_file: piece[%].unit_kind % not in dot_config vocab.unit_kind.* (cut_manifest_piece_schema_v1)', v_idx, v_uk_val;
  END IF;

Variable v_uk_val text declared at top of DECLARE block, assigned from v_piece->>'unit_kind' in the same line group as other piece-field reads.

All other behavior of fn_cut_mark_staged_file is identical:

  • status guard (v_req.status NOT IN ('copied','mark_rejected')) unchanged
  • transition mark_in_progress -> fn_iu_op_mark_file call -> transition marked -> signal — all unchanged
  • idempotency key cut-mark-<cut_request_id>-md5(p_pieces::text) — implicitly tightened since new schema rejects bad pieces before they ever reach the alias
  • return shape unchanged (piece_schema_validated='cut_manifest_piece_schema_v1')

md5: 67721b42a55315858e3c377654c2a5b1 -> e85065acb9996623e0ef1f654d991df6.

fn_iu_verify_mark — Axis D added

-- NEW: Axis D — every piece.unit_kind non-empty AND in live dot_config vocab.unit_kind.*
SELECT count(*) INTO v_uk_total FROM jsonb_array_elements(v_pieces);
SELECT count(*) INTO v_uk_missing FROM jsonb_array_elements(v_pieces) p
  WHERE (p->>'unit_kind') IS NULL OR btrim(p->>'unit_kind') = '';
SELECT count(*) INTO v_uk_in_vocab FROM jsonb_array_elements(v_pieces) p
  WHERE (p->>'unit_kind') IS NOT NULL
    AND btrim(p->>'unit_kind') <> ''
    AND EXISTS (SELECT 1 FROM public.dot_config dc
                WHERE dc.key = 'vocab.unit_kind.' || (p->>'unit_kind'));
v_axis_d := (v_uk_missing = 0) AND (v_uk_in_vocab = v_uk_total);
IF NOT v_axis_d THEN
  v_problems := v_problems || ARRAY[
    'Axis D: unit_kind compatibility failed (total='||v_uk_total
    ||', missing='||v_uk_missing
    ||', in_vocab='||v_uk_in_vocab
    ||') — every piece.unit_kind must exist in dot_config vocab.unit_kind.*'
  ];
END IF;

Return shapes updated to include axis_d_ok:

  • success: {'ok':true,'verdict':'approved','axis_a_ok':true,'axis_b_ok':true,'axis_c_ok':true,'axis_d_ok':true}
  • rejected: includes axis_d_ok field in rejection JSON
  • metadata updated on approval to include verify_mark_axis_d:true

Structured-verdict pattern is preserved (no hard RAISE — failures surface as problems[] per existing axes).

md5: 04e5191c430a142712bfe63089863d44 -> 1db15847b1c48e3b86568b712a15cfd6.

cut_manifest_piece_schema_v1 — formal contract (post-fix)

Required per piece (7 fields):

Field Type Constraint
local_piece_id text non-empty, unique within manifest
content_text text non-empty
canonical_address text non-empty
source_position text matching ^[0-9]+$ integer >= 1
piece_role text non-empty
section_type text non-empty AND dot_config vocab.section_type.<v> exists
unit_kind text non-empty AND dot_config vocab.unit_kind.<v> exists -- NEW

Optional per piece (1 field):

Field Type Constraint
parent_local_id text or null if not null, must resolve to another piece's local_piece_id in the same manifest (verify_mark Axis C)

Manifest-level (verify_mark only):

Field Constraint
source_position density dense from 1, no holes (Axis A)
manifest_digest 32-hex
coverage_proof.covered_bytes equals manifest.source_bytes
per-piece unit_kind all in dot_config vocab.unit_kind. (Axis D)* -- NEW

Refusal contract

Boundary Trigger Error pattern
MARK call (fn_cut_mark_staged_file) piece.unit_kind missing piece[N].unit_kind is required (cut_manifest_piece_schema_v1)
MARK call (fn_cut_mark_staged_file) piece.unit_kind not in vocab.unit_kind.* piece[N].unit_kind <V> not in dot_config vocab.unit_kind.* (cut_manifest_piece_schema_v1)
VERIFY_MARK call (fn_iu_verify_mark) any piece fails Axis D structured: {ok:false, verdict:'rejected', axis_d_ok:false, problems:[...'Axis D: ...']}

All errors are indexed (include piece index N) for caller convergence.

Boundary placement rationale

The MARK schema gate lives in fn_cut_mark_staged_file (the operational wrapper), NOT in fn_iu_op_mark_file (the governance-pinned alias whose md5 must remain ffaa47fff7a906d93060141661080cd4). This preserves the alias contract while tightening the operational pipeline.

VERIFY_MARK Axis D is the secondary defense: even if a future caller bypasses fn_cut_mark_staged_file and inserts a manifest directly into iu_staging_payload (which the data model does NOT prevent), the operator approval path via fn_iu_verify_mark will catch it before lifecycle_status='approved'.

Rollback path

-- one-shot recovery (loses unit_kind gate but is non-destructive)
BEGIN;
-- Re-CREATE prior bodies:
-- fn_cut_mark_staged_file md5 67721b42a55315858e3c377654c2a5b1 (6-field schema)
-- fn_iu_verify_mark md5 04e5191c430a142712bfe63089863d44 (3 axes)
-- Sources: /tmp/pre_dieu39_unit_kind_fix_20260527.sql (pg_dump, 921 MB, md5 2d21965b...)
COMMIT;

The pg_dump backup contains the verbatim prior CREATE OR REPLACE FUNCTION statements in the same single TX shape.

Forbiddens honored

  • No alias body changed (5/5 md5 pinned: c5d556bc / 66b813e5 / ffaa47ff / bf20bd19 / ac61dade)
  • No dot_config mutation (no vocab widen, no mapping row)
  • No table DDL
  • No pg_cron
  • No production_documents touch
  • No Qdrant touch
  • No event_outbox mutation
  • No gate flip
Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-cut-dieu39-unit-kind-root-cause-and-contract-fix/04-contract-fix.md