Pipeline ekstraksi dan penalaran multi-dokumen (seperti arsitektur Parsewise) sering mengalami kegagalan struktural saat mengolah data yang saling bergantung: faktur PDF, manifes pengiriman Markdown, dan metadata transaksi JSON. Perubahan minor pada parser hulu atau respons ekstraksi LLM dapat memicu schema drift yang merusak integritas relasional downstream. Menjalankan live parser atau model AI langsung di Continuous Integration (CI) memicu flaky test akibat latensi jaringan, rate limit, dan output non-deterministik.

Solusinya adalah uji kontrak multi-dokumen menggunakan artefak statis (golden fixtures) dan validasi skema relasional yang ketat. Pendekatan ini memastikan kompatibilitas skema lintas dokumen dapat diverifikasi secara instan di CI tanpa ketergantungan eksternal.

Penyebab Non-Determinisme dan Regresi Cross-Doc

Regresi pada pipeline multi-dokumen umumnya terjadi pada tiga titik:

  • Inkonsistensi Tipe Kunci Relasional: Parser PDF membaca ID faktur sebagai string "INV-001", sementara parser JSON mengekstrak referensi sebagai integer 1.
  • Perubahan Struktur Tanpa Versi: Upstream parser mengubah struktur hierarki (misalnya, membungkus array item dalam object baru) tanpa memicu breaking change pada level dokumen tunggal, tetapi merusak agregasi relasional.
  • Non-Determinisme Output LLM: Ekstraksi berbasis prompt sering menghasilkan variasi format tanggal (ISO 8601 vs UNIX timestamp) atau nilai float yang tidak seragam.

Strategi Isolasi Golden Fixture

Untuk menghindari pemanggilan eksternal di CI, pisahkan pengujian menjadi dua layer: layer ekstraksi (diuji via integrasi terjadwal) dan layer kontrak relasional (diuji di setiap pull request menggunakan fixture statis).

Struktur direktori fixture yang ideal mengelompokkan input mentah dan ekspektasi kanonikal per skenario:

tests/fixtures/contracts/order-batch-01/
├── input/
│   ├── invoice.pdf
│   ├── manifest.md
│   └── metadata.json
└── canonical/
    ├── invoice.extracted.json
    ├── manifest.extracted.json
    └── metadata.extracted.json

CI mengeksekusi assertion harness langsung pada artefak di dalam folder canonical/, menghilangkan faktor latensi parser pihak ketiga.

Validasi Relasi Cross-Doc Menggunakan Zod

Validasi skema tunggal tidak cukup untuk mendeteksi inkonsistensi relasional. Skema pengujian harus memverifikasi integritas referensial (foreign keys, total kuantitas agregat, dan penanggalan) antar dokumen.

import { z } from "zod";

// 1. Skema individual dokumen
export const InvoiceSchema = z.object({
  invoiceId: z.string(),
  currency: z.literal("IDR"),
  totalAmount: z.number().positive(),
  lineItems: z.array(z.object({
    sku: z.string(),
    quantity: z.number().int().positive(),
    price: z.number().positive(),
  })),
});

export const ManifestSchema = z.object({
  shipmentId: z.string(),
  invoiceReferenceId: z.string(),
  deliveredSkus: z.record(z.string(), z.number().int().nonnegative()),
});

// 2. Skema gabungan untuk validasi integritas relasional
export const MultiDocContractSchema = z.object({
  invoice: InvoiceSchema,
  manifest: ManifestSchema,
}).superRefine((data, ctx) => {
  // Verifikasi foreign key
  if (data.invoice.invoiceId !== data.manifest.invoiceReferenceId) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: `ID Referensi tidak cocok: ${data.manifest.invoiceReferenceId} != ${data.invoice.invoiceId}`,
      path: ["manifest", "invoiceReferenceId"],
    });
  }

  // Verifikasi rekonsiliasi kuantitas SKU cross-doc
  for (const item of data.invoice.lineItems) {
    const deliveredQty = data.manifest.deliveredSkus[item.sku];
    if (deliveredQty === undefined) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `SKU ${item.sku} pada invoice tidak ditemukan di manifes pengiriman`,
        path: ["manifest", "deliveredSkus", item.sku],
      });
    } else if (deliveredQty !== item.quantity) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `Kuantitas SKU ${item.sku} tidak cocok. Invoice: ${item.quantity}, Manifes: ${deliveredQty}`,
        path: ["manifest", "deliveredSkus", item.sku],
      });
    }
  }
});

type MultiDocContract = z.infer<typeof MultiDocContractSchema>;

Assertion Harness Sederhana untuk CI

Harness ini memuat fixture kanonikal dan menjalankan assertion skema secara lokal dalam hitungan milidetik.

import { readFileSync, readdirSync } from "fs";
import { join } from "path";
import { MultiDocContractSchema } from "./schemas";

function runContractTests(fixtureDir: string) {
  const scenarios = readdirSync(fixtureDir);
  let failures = 0;

  for (const scenario of scenarios) {
    const canonicalPath = join(fixtureDir, scenario, "canonical");
    try {
      const invoice = JSON.parse(readFileSync(join(canonicalPath, "invoice.extracted.json"), "utf-8"));
      const manifest = JSON.parse(readFileSync(join(canonicalPath, "manifest.extracted.json"), "utf-8"));

      const result = MultiDocContractSchema.safeParse({ invoice, manifest });

      if (!result.success) {
        console.error(`FAIL: Skenario [${scenario}] melanggar kontrak skema:`);
        console.error(JSON.stringify(result.error.format(), null, 2));
        failures++;
      } else {
        console.log(`PASS: Skenario [${scenario}] valid.`);
      }
    } catch (err) {
      console.error(`ERROR: Gagal memproses skenario [${scenario}]:`, err);
      failures++;
    }
  }

  if (failures > 0) {
    process.exit(1);
  }
}

// Eksekusi assertion pada fixture terisolasi
runContractTests("./tests/fixtures/contracts");

Trade-offs dan Praktik Terbaik

  • Kelemahan Golden Fixtures: Fixture statis tidak menangkap edge-case format baru yang muncul di lingkungan produksi. Mitigasi: Implementasikan pipeline otomatis untuk mengambil 1% payload anonim dari produksi dan mengubahnya menjadi fixture uji baru jika validasi skema lolos.
  • Pemisahan CI Stages: Jalankan uji kontrak deterministik ini pada level PR check (fast-fail, target waktu eksekusi < 5 detik). Jadwalkan uji end-to-end yang memanggil engine parser aktual secara berkala (cron job/nightly build) untuk mengidentifikasi upstream drift tanpa memperlambat developer loop.