Mirror dokumen pindah kantor: dari work item ke Plane Pages
Halaman API Plane bikin mirror dokumen versi work item terasa salah tempat. Kontrak penyimpanannya berubah, desain identitas mirror ikut dibongkar.
Ringkasan
Gue kira tinggal ganti endpoint Pages beres, ternyata kontrak storagenya beda total bikin desain mirror harus dibongkar. Di Pages HTML disimpen verbatim jadi marker bisa ngumpet di komen, kalau update itu ngereplace semua bukan nambahin. Halamannya datar tanpa folder, hapus harus diarsip dulu, dan gambar tetep nangkring di git aja.
Saya membuka diff spesifikasi baru di terminal dan teringat kondisi lama: mirror dokumen versi sebelumnya memaksa setiap berkas masuk ke papan kanban sebagai work item biasa. Dokumen nabrak ke kolom-kolom tugas, lengkap dengan status dan assignee. Itu aneh. Dokumen bukan pekerjaan yang perlu digeser ke kolom Done; dokumen perlu dibaca dan diedit sebagai dokumen.
Tebakan awal saya optimis: tinggal tambahkan endpoint pages ke engine lama, ganti payload, beres. Ternyata salah besar. Yang berubah bukan alamat endpoint-nya, melainkan kontrak penyimpanannya, dan kontrak itulah yang memaksa saya membongkar ulang desain identitas mirror.
Kontrak yang membalik arah
Pelajaran utamanya saya rumuskan begini: mirror sinkronisasi hanya bisa dipercaya sejauh endpoint yang paling sedikit memodifikasi data. Jadi probe storage dulu, baru biarkan storage membentuk desain identitas, bukan sebaliknya.
Di versi lama, work item melakukan sanitasi HTML di sisi server memakai allowlist nh3. Komentar HTML langsung dilucuti, fragmen multi-elemen dibungkus ulang. Makanya marker sinkronisasi v2 terpaksa menjadi baris pertama yang terlihat jelas di dalam teks, karena satu-satunya bentuk yang dijamin survive adalah teks polos. Saat saya probe Pages di edisi komunitas, hasilnya membalik semua asumsi itu: HTML di-round-trip verbatim, termasuk komentar HTML. Marker v3 akhirnya bisa bersembunyi di dalam komentar pembuka tanpa digigit sanitizer.
Aturan main create/update-nya juga spesifik. Saat membuat halaman, description_html itu wajib di body [1]. Saat update, description_html mengganti seluruh isi halaman saat update, bukan menambah di akhir; halaman yang terkunci atau diarsipkan menolak di-update, dan layanan dokumen kolaboratif bisa membalas 502 atau 503 [2]. Kalau mesin sinkronisasi saya masih berasumsi semantik append, satu push bisa menghapus setengah dokumen tanpa pesan error.
Struktur hierarki ternyata ilusi. Parameter parent_id diabaikan saat create, jadi halaman di sistem ini datar. Konvensi penamaan yang menggantikan pohon folder: notulen memakai format MEETING: tanggal - judul, dokumen perpustakaan memakai DOC: path-relatif. Daftar halaman cuma mengembalikan envelope tanpa body, artinya pengecekan status butuh satu request retrieve per halaman. Trade-off yang saya terima, karena gate cukup memakai kolom updated_at dari envelope itu tanpa menyentuh body sama sekali.
Siklus hidup halaman juga punya jebakan. Menghapus halaman yang masih aktif langsung dibalas 400; halaman wajib diarsipkan lebih dulu [4]. Restore justru berbentuk operasi DELETE pada sub-path /archive/, dan halaman induk harus dipulihkan sebelum anaknya [5]. Dari probe sendiri saya menambahkan satu temuan yang tidak tertulis di docs: halaman yang sudah terhapus justru membalas retrieve dengan 403, jadi alur recreate memperlakukan 403 itu sebagai sinyal halaman-hilang, bukan crash. Ditambah lagi, flag is_locked memblokir semua upaya update, dan engine wajib melaporkannya sebagai blocked, bukan memaksa jalan.
Dokumentasi resminya justru lebih konservatif daripada server yang saya probe. Halaman kontrak konten memperingatkan bahwa Plane melakukan sanitasi HTML, menyarankan fetch ulang setelah write kalau integrasi perlu memeriksa HTML akhir yang tersimpan, dan menutup payload di 10 MB [3]. Probe live saya balik byte-identical. Dokumen berperan sebagai plafon konservatif, probe sebagai kenyataan hari ini; keduanya saya bawa masuk desain, dengan verifikasi pasca-tulis sebagai pengaman.
Ada satu detail kecil yang menguatkan pola yang sama: gambar. Pages di edisi komunitas tidak punya jalur unggah lampiran sama sekali, dan berkas biner tetap tinggal di git. Alih-alih memalsukan upload, referensi gambar  di markdown dirender sebagai penanda terlihat yang menunjuk jalur berkas di repo, dan proses pull memetakan penanda itu kembali ke bentuk semula. Teks alt bahkan dikanonikalisasi hilang, supaya bolak-balik markdown ke HTML dan kembali tetap identik byte demi byte. Pesannya jelas: kalau sistem target tidak bisa menyimpan sesuatu, jangan pura-pura bisa; arahkan pembaca ke sumber kebenarannya.
Dua tier, hash sebagai otoritas
Pembagian wilayahnya sederhana: notulen meeting berjalan dua arah supaya bisa diedit dari ponsel, dokumen perpustakaan lainnya murni publikasi satu arah dari git, dan tarik-balik (pull) ditolak by design dengan pesan penjelasan. Git tetap satu-satunya editor untuk perpustakaan; kalau ada yang mengubah halaman dari sisi Plane, statusnya menyala sebagai alarm divergensi, lengkap dengan kedua hash yang tidak cocok.
Otoritas kebenaran ada di hash: sha256 dari body markdown yang dinormalisasi dihitung di kedua sisi, dan itulah penentu status. Nama halaman dan timestamp hanya alat diagnostik. Marker yang jalurnya tidak cocok dengan konfigurasi langsung dilaporkan blocked, dan satu pintu reconcile di plane-init yang menaikkan config v2 ke v3 tanpa pernah memperbaiki mapping secara diam-diam.
Bagian yang paling saya suka adalah property test-nya: untuk setiap fixture dan setiap berkas di pohon dokumen, markdown ke HTML lalu kembali ke markdown harus sama dengan markdown ternormalisasi. Invarian ini yang membuat konversi deterministik bisa dipercaya, dan kalau suatu hari subset konversi melebar, test inilah yang berteriak lebih dulu sebelum mirror sempat merusak dokumen siapa pun.