KB-47A6
90000x · 04 — Verify MARK (Migration 039)
9 min read Revision 1
iu-core90000xverify-markfn_iu_verify_markdot_iu_verify_mark_manifestaxis-a-b-cmigration-039authored-ready2026-05-25
90000x · 04 — Verify MARK
Phase: D
Status: AUTHORED-READY
Migration: 039
D9 delta after apply: +1 fn (fn_iu_verify_mark), +1 DOT (dot_iu_verify_mark_manifest).
Purpose
Transition pending_review → approved or rejected, with axis A/B/C structural checks and coverage byte-match. Only approved rows are eligible for cut by fn_iu_cut_from_manifest. CHECK in mig 037 will refuse CUT without approved_at + approval_doc_id.
What it checks
| Check ID | What | Failure raises problem |
|---|---|---|
| M-pieces | manifest.pieces is non-empty JSON array |
manifest.pieces missing or empty |
| Axis A | source_position values dense + monotonic from 1 (min=1, holes=0) |
Axis A: positions not dense from 1 (min=X, max=Y, holes=Z) |
| Axis B | every piece has both piece_role (vocab from fn_iu_create) AND section_type |
Axis B: piece_role or section_type missing on at least one piece |
| Axis C | every parent_local_id (if set) resolves to a sibling local_piece_id within the manifest |
Axis C: parent_local_id resolves outside manifest |
| Coverage | coverage_proof.covered_bytes == manifest.source_bytes |
coverage_proof.covered_bytes ≠ manifest.source_bytes |
| Digest | manifest_digest matches ^[0-9a-f]{32}$ (32-hex; full canonical-JSON recompute is operator side per doc 03 §3 whitespace_collapse_v1) |
manifest_digest is not 32-hex |
Migration 039 — authored
-- IU Core 90000x · Migration 039 · Verify MARK
BEGIN;
CREATE OR REPLACE FUNCTION fn_iu_verify_mark(
p_staging_record_id uuid,
p_apply boolean DEFAULT false,
p_approval_doc_id text DEFAULT NULL,
p_approver text DEFAULT NULL,
p_actor text DEFAULT 'fn_iu_verify_mark'
) RETURNS jsonb LANGUAGE plpgsql AS $$
DECLARE
v_rec iu_core.iu_staging_record;
v_manifest jsonb; v_coverage jsonb; v_pieces jsonb;
v_positions int[]; v_min int; v_max int; v_holes int;
v_problems text[] := '{}';
v_axis_a boolean := false; v_axis_b boolean := false; v_axis_c boolean := false;
BEGIN
SELECT * INTO v_rec FROM iu_core.iu_staging_record WHERE staging_record_id=p_staging_record_id;
IF NOT FOUND THEN RETURN jsonb_build_object('ok',false,'reason','not_found'); END IF;
IF v_rec.staging_kind <> 'mark_manifest' THEN
RETURN jsonb_build_object('ok',false,'reason','not a mark_manifest','staging_kind',v_rec.staging_kind);
END IF;
IF v_rec.lifecycle_status <> 'pending_review' THEN
RETURN jsonb_build_object('ok',false,'reason','lifecycle must be pending_review','live',v_rec.lifecycle_status);
END IF;
SELECT payload_json INTO v_manifest FROM iu_core.iu_staging_payload
WHERE staging_record_id=p_staging_record_id AND part_name='cut_manifest';
SELECT payload_json INTO v_coverage FROM iu_core.iu_staging_payload
WHERE staging_record_id=p_staging_record_id AND part_name='coverage_proof';
v_pieces := v_manifest->'pieces';
IF v_pieces IS NULL OR jsonb_typeof(v_pieces) <> 'array' OR jsonb_array_length(v_pieces) = 0 THEN
v_problems := v_problems || 'manifest.pieces missing or empty';
ELSE
SELECT array_agg((p->>'source_position')::int ORDER BY (p->>'source_position')::int)
INTO v_positions FROM jsonb_array_elements(v_pieces) p;
v_min := v_positions[1]; v_max := v_positions[array_length(v_positions,1)];
v_holes := v_max - v_min + 1 - array_length(v_positions,1);
v_axis_a := (v_min=1) AND (v_holes=0);
IF NOT v_axis_a THEN v_problems := v_problems || ('Axis A: positions not dense from 1 (min='||v_min||', max='||v_max||', holes='||v_holes||')'); END IF;
v_axis_b := NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(v_pieces) p
WHERE (p->>'piece_role') IS NULL OR (p->>'section_type') IS NULL
);
IF NOT v_axis_b THEN v_problems := v_problems || 'Axis B: piece_role or section_type missing on at least one piece'; END IF;
v_axis_c := NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(v_pieces) p
WHERE (p->>'parent_local_id') IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM jsonb_array_elements(v_pieces) p2
WHERE p2->>'local_piece_id' = p->>'parent_local_id'
)
);
IF NOT v_axis_c THEN v_problems := v_problems || 'Axis C: parent_local_id resolves outside manifest'; END IF;
END IF;
IF (v_coverage->>'covered_bytes') IS NULL
OR (v_manifest->>'source_bytes') IS NULL
OR (v_coverage->>'covered_bytes')::bigint <> (v_manifest->>'source_bytes')::bigint THEN
v_problems := v_problems || 'coverage_proof.covered_bytes ≠ manifest.source_bytes';
END IF;
IF (v_manifest->>'manifest_digest') !~ '^[0-9a-f]{32}$' THEN
v_problems := v_problems || 'manifest_digest is not 32-hex';
END IF;
IF array_length(v_problems,1) IS NOT NULL THEN
IF p_apply THEN
UPDATE iu_core.iu_staging_record
SET lifecycle_status='rejected',
metadata = metadata || jsonb_build_object('verify_mark_problems', to_jsonb(v_problems), 'verify_mark_at', now())
WHERE staging_record_id=p_staging_record_id;
INSERT INTO dot_iu_command_run (command_name,payload_json,actor,status)
VALUES ('dot_iu_verify_mark_manifest',
jsonb_build_object('staging_record_id',p_staging_record_id,'result','rejected','problems',to_jsonb(v_problems)),
p_actor,'ok');
END IF;
RETURN jsonb_build_object('ok',false,'verdict','rejected',
'axis_a_ok',v_axis_a,'axis_b_ok',v_axis_b,'axis_c_ok',v_axis_c,
'problems', to_jsonb(v_problems));
END IF;
IF p_apply THEN
IF p_approval_doc_id IS NULL OR p_approver IS NULL THEN
RETURN jsonb_build_object('ok',false,'reason','approval_doc_id and p_approver required for apply=true');
END IF;
UPDATE iu_core.iu_staging_record
SET lifecycle_status='approved',
approved_at = now(), approved_by = p_approver, approval_doc_id = p_approval_doc_id,
metadata = metadata || jsonb_build_object('verify_mark_axis_a',true,'verify_mark_axis_b',true,'verify_mark_axis_c',true,'verify_mark_at',now())
WHERE staging_record_id=p_staging_record_id;
INSERT INTO dot_iu_command_run (command_name,payload_json,actor,status)
VALUES ('dot_iu_verify_mark_manifest',
jsonb_build_object('staging_record_id',p_staging_record_id,'result','approved','approval_doc_id',p_approval_doc_id),
p_actor,'ok');
END IF;
RETURN jsonb_build_object('ok',true,'verdict','approved','axis_a_ok',true,'axis_b_ok',true,'axis_c_ok',true);
END; $$;
INSERT INTO dot_iu_command_catalog (command_name, category, mutating, reversible, target_functions, registered_at)
VALUES ('dot_iu_verify_mark_manifest', 'verify', true, false, ARRAY['fn_iu_verify_mark']::text[], now());
COMMIT;
Rollback 039
BEGIN;
DELETE FROM dot_iu_command_catalog WHERE command_name='dot_iu_verify_mark_manifest';
DROP FUNCTION IF EXISTS fn_iu_verify_mark(uuid,boolean,text,text,text);
COMMIT;
Dry-run pattern
-- dry-run on a pending_review row, just inspect verdict
SELECT fn_iu_verify_mark(p_staging_record_id := '<uuid>', p_apply := false);
Result shape (PASS):
{"ok": true, "verdict": "approved", "axis_a_ok": true, "axis_b_ok": true, "axis_c_ok": true}
Result shape (FAIL):
{"ok": false, "verdict": "rejected", "axis_a_ok": false, "axis_b_ok": true, "axis_c_ok": true,
"problems": ["Axis A: positions not dense from 1 (min=2, max=4, holes=0)"]}
Operator approval requirement
p_apply=true requires BOTH p_approval_doc_id (KB-immutable path like knowledge/.../approval-2026-05-25.md) AND p_approver (operator handle). This propagates into approved_at + approved_by + approval_doc_id on the staging row — the tighter consumed-consistency CHECK in 037 then refuses any CUT that lacks those fields.
Cross-links
- [[project_dot_iu_cutter_v0_6_iu_core_80000x_operational_cut_workflow_mark_review_cut_verify_mark_review_cut_verify]] —
04-review-approval-checklist.mdis the R0-R11 operator checklist that maps toapproval_doc_id. - [[feedback-section-type-and-piece-role-vocab-enforced]] — Axis B vocab discovered from live CHECKs.
- 06-three-axis-requirements.md of 80000x — full axis A/B/C invariants.