Ringkasan Utama
Konvensi tertulis bukanlah batasan. Mengubah aturan arsitektur menjadi script yang menggagalkan build adalah yang membuatnya nyata, dan konversi itu hanya sepadan untuk aturan yang menanggung beban, sering dilanggar, dan bisa diperiksa secara mekanis. Tiga gate prebuild di situs ini memeriksa 49 route entry dalam waktu di bawah tiga detik.
Saya menambahkan dunia 3D yang bisa dijelajahi ke sebuah situs portofolio yang halaman lainnya diukur dalam kilobyte. Aturannya mudah dirumuskan, dan saya menuliskannya di file instruksi repository: game ini tidak boleh membebani halaman yang bukan game. Merumuskannya adalah bagian yang mudah. Satu kalimat di file markdown tidak punya cara untuk gagal, dan three.js justru jenis dependency yang masuk lewat import yang tidak diperiksa siapa pun.
Tulisan ini tentang mengubah kalimat itu menjadi script yang keluar dengan exit code bukan nol, lewat tiga gate yang sekarang berjalan sebelum setiap build situs ini. Saya bahas apa yang dibuktikan masing-masing, berapa biaya menulisnya, dan satu gate yang sempat rilis dalam bentuk lemah, lolos bersih berbulan-bulan, dan selama itu menyembunyikan 168 tanggal yang salah.
Tiga lapis penegakan, dan celah di antaranya
Panduan BairesDev tentang maintainable code menyusun lapisan penegakan dalam urutan yang biasanya dibangun tim, dan urutannya memang tepat. Setiap lapis membuat lapis berikutnya lebih murah:
- Static analysis, linter dan formatter menangkap masalah mekanis saat kode ditulis, sehingga tidak ada yang menghabiskan perhatian review untuk itu.
- Kebersihan commit dan pull request memberi reviewer konteks untuk menilai perubahan, bukan menyusun ulang ceritanya sendiri.
- Code review adalah lapis yang menangkap sisanya, dan juga yang paling mahal — justru itulah alasan dua lapis sebelumnya ada.
Celahnya ada di bawah ketiganya. Setiap lapis itu memeriksa diff yang ada di depan manusia. Tidak satu pun tahu apa pun tentang arsitektur Anda. ESLint tahu JavaScript; ia tidak tahu bahwa three.js hanya milik satu route di situs ini, dan bahwa satu import dari nav bar akan membatalkan seluruh pengaturan itu. Aturan tersebut bukan code smell. Itu fakta tentang repository ini, dan satu-satunya tempat ia bisa hidup adalah di script milik repository ini sendiri.
Grep tidak bisa melihat reachability
Versi aturan yang bisa ditegakkan grep adalah versi lemahnya. Memeriksa bahwa three.js hanya di-import di dalam direktori game hanyalah pencarian string, kira-kira dua puluh baris, dan itu menangkap kesalahan yang kasat mata. Yang tidak tertangkap justru kesalahan yang benar-benar menambah berat halaman.
Aturan yang penting adalah tidak ada route entry yang boleh mencapai engine, dan mencapai itu pertanyaan graph, bukan pertanyaan teks. Satu page yang meng-import satu komponen game yang tampak tidak berbahaya, yang meng-import world, yang meng-import engine, akan mengirim three.js ke first load route tersebut. Tidak ada apa pun di file page itu yang menunjukkannya. Gate harus menelusuri import graph, dan hanya boleh menelusuri edge statis, karena dynamic import justru itulah yang menahan engine keluar dari bundle:
// Static import/export with a from-clause only.
// A bare import("x") has no from-clause, so dynamic imports are cut
// points here by construction — exactly as they are in the bundler.
const STATIC_IMPORT =
/(?:^|\n)\s*(?:import|export)\b([\s\S]*?)from\s*["']([^"']+)["']/g;
function staticImports(file) {
const source = stripComments(fs.readFileSync(file, "utf8"));
const specifiers = [];
for (const [, clause, specifier] of source.matchAll(STATIC_IMPORT)) {
// A type-only import is erased by the compiler and carries no weight.
if (/^\s*type\b/.test(clause)) continue;
specifiers.push(specifier);
}
return specifiers;
}Detail terakhir itulah yang membuat pemeriksaan ini jujur, bukan sekadar perkiraan. Pemanggilan import biasa tidak punya from-clause, jadi tidak pernah cocok dengan pola — artinya dynamic import otomatis menjadi titik potong dalam penelusuran, persis di tempat bundler memotong. Gate menelusuri dari seluruh 49 file route entry di situs ini dan melaporkan rantai import yang bermasalah, bukan cuma nama file, sehingga perbaikannya jelas. Import bertipe type saja dilewati dengan alasan yang sama seperti compiler menghapusnya: tidak menambah berat apa pun.
Gate yang memeriksa hal yang murah
Registry blog di situs ini terpisah di dua module: satu menyimpan data kartu yang dilihat pembaca, satu lagi menyimpan metadata SEO yang dibaca Google. Keduanya harus sinkron. Versi pertama gate parity membuktikan keduanya sinkron dengan membandingkan dua himpunan key — pemeriksaan yang paling kentara sekaligus paling murah. Gate itu rilis, lolos di setiap build, dan tidak menangkap apa pun.
Tidak menangkap apa pun karena himpunan key memang tidak pernah bergeser. Nilainya yang bergeser. Tanggal yang dilihat pembaca di kartu berasal dari month key dan tahun; tanggal yang dilihat Google berasal dari published date terpisah yang mengisi sitemap dan structured data. Ketika akhirnya saya bandingkan nilainya, bukan key-nya, 168 dari 386 artikel tidak cocok, beberapa meleset sepuluh bulan. Sebuah kartu tertulis Mei 2025 sementara structured data-nya sendiri menyebut Juli 2024. Tidak pernah ada yang memunculkannya, karena kedua nilai itu sah kalau dilihat sendiri-sendiri.
// Version 1: the two key sets agree. Shipped, passed, caught nothing.
const missingMeta = [...staticIds].filter((id) => !metaIds.has(id));
// Version 2: the values agree too. The date a reader sees comes from
// monthKey + year; the date Google sees comes from datePublished.
// Both were individually valid, which is why nothing ever surfaced it.
const dateMismatches = BLOG_POSTS_STATIC.filter((post) => {
const meta = BLOG_META[post.id];
if (!meta) return false;
const [year, month] = meta.datePublished.split("-");
return post.year !== year || post.monthKey !== monthKeyFor(month);
});Gate yang hijau bukan bukti invariant-nya sehat
Centang hijau hanya memberi tahu bahwa assertion yang Anda tulis itu benar. Ia tidak mengatakan apa pun tentang apakah yang Anda assert sudah tepat. Gate parity itu hijau sepanjang masa lemahnya. Saat menulis gate, tanyakan seperti apa bentuk kegagalan yang ingin dicegah itu di dalam data, lalu pastikan gate akan menangkap bentuk spesifik tersebut — paling meyakinkan dengan sengaja merusak sesuatu dan menonton gate-nya gagal.
Berapa biaya gate-gate ini sebenarnya
Penegakan tidak gratis, dan cara jujur untuk memutuskannya adalah dengan menghitung harganya. Berikut setiap aturan yang dimekanisasi situs ini, apa yang dibuktikan, dan berapa ongkosnya:
| Gate | Apa yang dibuktikan | Berapa biayanya |
|---|---|---|
| Lokasi engine | three.js hanya di-import di dalam direktori milik game | Pencarian string, sekitar 20 baris |
| Reachability route | Tidak ada route entry yang mencapai engine lewat import statis | Penelusuran graph depth-first, bagian terbesar dari file 282 baris |
| Prefetch link | Setiap link ke game menonaktifkan viewport prefetch | Pemindaian tag yang sadar kurung kurawal, sekitar 40 baris |
| Parity registry | Tanggal tampil setiap artikel cocok dengan structured data-nya | Sekitar 40 baris, tinggal di dalam module yang dijaganya |
Seluruh rangkaian berjalan sekitar dua setengah detik di setiap build. Angka itu lebih penting daripada jumlah baris: gate yang menambah satu menit ke waktu build akan dimatikan saat insiden pertama, dan tidak pernah kembali. Pemeriksaan prefetch adalah yang paling sulit saya bela kalau berdiri sendiri — tapi nav dan footer dirender di setiap halaman, jadi satu opt-out yang terlewat akan menarik route chunk game ke seluruh situs, sementara pemeriksaannya cuma empat puluh baris. Penegakan murah untuk aturan yang dampaknya luas adalah pertukaran yang mudah diambil.
Aturan mana yang layak dimekanisasi
Sebagian besar konvensi sebaiknya tetap jadi konvensi. Menulis gate untuk aturan yang rusak dua kali dalam sepuluh tahun adalah beban pemeliharaan yang menyamar sebagai jaring pengaman. Empat pertanyaan yang menentukan, dan sebuah aturan harus lolos keempatnya:
- Apakah menanggung beban? Kegagalannya harus benar-benar merugikan — berat halaman di semua route, tanggal salah di sitemap, satu locale rusak di production. Aturan yang sifatnya selera tidak termasuk.
- Apakah senyap saat dilanggar? Aturan yang langsung menunjukkan diri lewat test merah atau bug visual sudah punya gate. Mekanisasi yang gagal diam-diam dan bertahan berbulan-bulan.
- Apakah bisa diputuskan secara mekanis? Kalau penegakannya butuh penilaian soal maksud, script akan menghasilkan false positive, dan gate yang terlalu sering salah alarm akan dilewati lalu dihapus.
- Apakah memeriksanya lebih murah daripada memperdebatkannya lagi? Kalau aturan itu diperdebatkan ulang di review tiap beberapa minggu, script-nya sudah balik modal dari perhatian yang dihemat saja.
Aturan reachability lolos keempatnya, dan itulah alasan ia pantas dibuatkan penelusuran graph. Aturan tentang urutan import tidak lolos satu pun, dan itulah alasan ia tetap urusan linter, atau tidak diurus sama sekali.
Di mana otomasi benar-benar berhenti
Artikel BairesDev berargumen bahwa otomasi tidak bisa menegakkan maksud arsitektural maupun penamaan yang menyampaikan makna bisnis, dan bahwa keduanya butuh penilaian manusia. Soal penamaan, itu persis sesuai pengalaman saya. Soal maksud arsitektural, saya menarik garisnya di tempat lain: maksud yang sudah disederhanakan menjadi klaim struktural bisa dimekanisasi, dan penyederhanaan itulah sebagian besar pekerjaannya. Klaim bahwa engine tidak boleh dicapai route entry adalah maksud arsitektural, sekaligus properti graph yang bisa diputuskan script.
Yang tetap jadi urusan manusia adalah semua yang ada sebelum penyederhanaan itu — memutuskan bahwa game layak punya route sendiri, bahwa batas lazy adalah bentuk yang tepat, bahwa pertukarannya memang sepadan. Script tidak bisa memberi tahu Anda bahwa sebuah module boundary mengikuti garis yang salah. Ia hanya bisa menjaga batas yang sudah Anda pilih. Jadi pembagian yang jujur bukan otomasi lawan penilaian; melainkan penilaian dulu, lalu otomasi supaya penilaian itu tidak terkikis diam-diam.
Pasang di jalur yang tidak bisa dilewati
Sebuah gate hanya sekuat jalur pemanggilan terlemahnya. Gate-gate ini berjalan dari hook prebuild, bukan dari job CI tersendiri, sehingga ikut berjalan di setiap build, di mesin saya maupun di pipeline, tanpa perlu ada yang mengingat satu langkah tambahan:
// package.json — prebuild runs on every "npm run build",
// locally and in CI, with no separate workflow step to forget.
"scripts": {
"prebuild": "node scripts/generate-blog-sources.mjs --check && node scripts/game/check-placements.mjs && node scripts/game/check-imports.mjs",
"build": "next build"
}
// A gate nobody can skip beats a thorough one wired to a job
// that a reviewer is allowed to mark "not required".Hal ini lebih penting daripada kelengkapan pemeriksaan. Job CI terpisah bisa ditandai tidak wajib, dilewati saat hotfix, atau diam-diam dihapus ketika merah karena hal yang tidak berhubungan di suatu Jumat sore. Langkah prebuild menggagalkan build itu sendiri, jadi satu-satunya cara melewatinya adalah memperbaiki masalahnya atau menghapus pemeriksaannya secara sadar — dan penghapusan itu jadi diff yang terlihat dan ditinjau orang. Pesan kegagalannya juga bagian dari desain: masing-masing menyebut file yang bermasalah, rantai import yang menuju ke sana, dan apa yang harus dilakukan, karena gate yang cuma bilang tidak boleh tidak mengajari siapa pun.
Aturan yang tidak bisa Anda langgar tanpa sengaja
Katalog di situs ini sekarang lewat 590 artikel ditambah satu route 3D, dan saya berhenti mengandalkan ingatan sendiri untuk invariant apa pun yang penting. Ujinya sederhana: kalau sebuah aturan benar-benar merugikan saat dilanggar, dan pelanggarannya senyap, tempatnya bukan di file markdown yang cuma bisa dibaca. Tempatnya di script yang keluar dengan exit code bukan nol. Tulis aturannya sekali untuk manusia, lalu tulis sekali lagi untuk mesin, dan biarkan mesin yang tidak pernah lelah.
Sumber dan bacaan lanjutan

