Skip to content
This repository was archived by the owner on Aug 11, 2026. It is now read-only.

fix(cache): invalidasi edge cache tidak pernah benar-benar terjadi (#359) - #361

Merged
ahliweb merged 1 commit into
mainfrom
fix/359-invalidation-silent-noop
Jul 25, 2026
Merged

ahliweb merged 1 commit into
mainfrom
fix/359-invalidation-silent-noop

Conversation

@ahliweb

@ahliweb ahliweb commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Ditemukan saat memverifikasi #360 di staging: saya terbitkan artikel sungguhan, dan cache tetap HIT.

Akar masalahnya

Klien purge memakai metode HTTP kustom BAN — idiom Varnish yang lazim, dan yang saya kirim di #353 tanpa pernah menjalankannya lewat jalur aplikasi sungguhan.

fetch milik Bun diam-diam menulis ulang metode yang tidak dikenal menjadi GET. Diverifikasi pada Bun 1.3.14 terhadap Varnish sungguhan:

-   ReqMethod      GET
-   BereqMethod    GET

Permintaan tidak pernah menyentuh cabang ban di VCL, dilayani sebagai halaman biasa, dan membalas 200. Karena 200 itu, klien melaporkan purge berhasil. Hasilnya: invalidasi yang sepenuhnya mati namun tampak sehat — dari kode, dan dari bun run edge-cache:health, yang memakai metode kustom yang sama sehingga ikut buta.

Perbaikannya menutup kelasnya, bukan gejalanya

  1. Transport jadi POST /__awcms-edge-cache/ban. Tidak lagi bergantung pada metode kustom yang harus selamat melewati setiap klien HTTP di rantai — sesuatu yang lapisan ini tidak bisa verifikasi saat runtime, jadi lebih baik tidak diandalkan.
  2. Respons ban wajib membawa penanda X-Edge-Cache-Ban: ok. Sebuah 200 tanpa penanda kini dilaporkan gagal. Ini bagian yang penting: tanpa penanda, respons 200 dari apa pun — termasuk aplikasi yang kebetulan menjawab — akan terus terbaca sebagai sukses. Sekarang tidak bisa.

Verifikasi pada Varnish sungguhan

Yang diuji Hasil
MISS → HIT ✓
POST ban dengan token 200 + X-Edge-Cache-Ban: ok
Permintaan setelahnya MISS
GET ke path ban 405
POST tanpa token 403

Kenapa unit test tidak menangkapnya

Karena mereka men-stub fetch — persis lapisan yang rusak. Dua test baru menutupnya dari sisi kontrak: satu menegaskan permintaan benar-benar POST ke path yang dituju, satu menegaskan 200 tanpa penanda dibaca sebagai gagal.

bun run check hijau: 3729 pass, 0 fail.

🤖 Generated with Claude Code

)

Klien purge memakai metode HTTP kustom `BAN` — idiom Varnish yang lazim,
dan yang saya kirim di #353 tanpa pernah menjalankannya lewat jalur
aplikasi sungguhan.

`fetch` milik Bun DIAM-DIAM MENULIS ULANG metode yang tidak dikenal menjadi
`GET`. Diverifikasi pada Bun 1.3.14 terhadap Varnish sungguhan: permintaan
tiba sebagai `ReqMethod GET`, tidak pernah menyentuh cabang ban di VCL,
dilayani sebagai halaman biasa, dan membalas 200. Karena 200 itu, klien
melaporkan purge BERHASIL padahal cache tidak pernah tersentuh —
invalidasi yang sepenuhnya mati namun tampak sehat, baik dari kode maupun
dari `bun run edge-cache:health` (yang memakai metode kustom yang sama).

Dua perubahan menutup kelas kegagalan ini, bukan hanya gejalanya:

1. Transport menjadi `POST /__awcms-edge-cache/ban`. Tidak lagi bergantung
   pada metode kustom yang harus selamat melewati setiap klien HTTP di
   rantai — sesuatu yang lapisan ini tidak bisa verifikasi saat runtime.
2. Respons ban membawa penanda `X-Edge-Cache-Ban: ok` yang WAJIB ada.
   Sebuah 200 tanpa penanda kini dilaporkan GAGAL, bukan sukses: artinya
   yang menjawab bukan handler ban cache, melainkan sesuatu yang lain
   (biasanya aplikasi itu sendiri). Inilah yang membuat "sukses palsu"
   tidak mungkin terulang.

Diverifikasi pada Varnish sungguhan: MISS -> HIT -> POST ban (200 +
penanda) -> MISS; GET ke path ban 405; POST tanpa token 403.

Unit test tidak bisa menangkap ini karena mereka men-stub `fetch`; dua test
baru menutupnya dari sisi kontrak — satu menegaskan permintaan benar-benar
POST ke path yang dituju, satu menegaskan 200 tanpa penanda dibaca sebagai
gagal.

Refs #359

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@ahliweb
ahliweb merged commit d2798fb into main Jul 25, 2026
8 checks passed
@ahliweb
ahliweb deleted the fix/359-invalidation-silent-noop branch July 25, 2026 13:54
ahliweb added a commit that referenced this pull request Jul 25, 2026
…CI (#363)

Tindak lanjut post-mortem #359/#361. Invalidasi sempat mati total selama
dua rilis sementara empat pengaman melaporkan sehat — dan keempatnya
ternyata satu asumsi yang sama: bahwa permintaan yang ditulis adalah
permintaan yang terkirim. Unit test men-stub `fetch` (persis lapisan yang
rusak), `edge-cache:health` memakai klien yang sama dengan yang
diperiksanya, klien itu menyimpulkan sukses dari status code, dan metrik
ikut mencatat "purged" untuk non-purge.

Dua penutup yang berdiri di luar asumsi itu:

- tests/integration/edge-cache-varnish.integration.test.ts menyalakan
  varnish:7.7.3 sungguhan dari deploy/varnish/default.vcl yang dikirim
  (hanya alamat backend yang ditukar, substitusi yang sama dengan skrip
  repoint staging), lalu membuktikan purge benar-benar membuang objeknya
  dan origin kembali dipukul. CI menjalankannya dengan
  EDGE_CACHE_VARNISH_TEST=1 sehingga ketiadaan Docker menjadi gagal keras,
  bukan skip diam-diam — suite yang menjaga transport tidak boleh lulus
  dengan cara tidak dijalankan.
- `bun run edge-cache:verify -- --url=<url>` memanaskan URL sampai HIT,
  mem-purge, lalu mewajibkan MISS. Sebuah MISS saja tidak membuktikan apa
  pun (kedaluwarsa TTL identik), jadi HIT beberapa detik sebelumnya itulah
  yang membuat urutan ini sahih.

Suite baru diuji balik dengan mengembalikan transport lama sementara:
4 dari 8 test gagal, termasuk satu yang melaporkan `purged` padahal
tokennya salah.

bun run check hijau: 3737 pass, 0 fail.

Co-authored-by: AWCMS-Micro Security <security@awcms-micro>
ahliweb added a commit that referenced this pull request Jul 26, 2026
…369/#370/#371/#372 + bagian in-repo #296) (#374)

* refactor(lib): tetapkan batas src/lib dan pindahkan kode presentasi modul ke presentation/

`src/lib/` tumbuh menjadi sistem modul kedua yang tidak dijaga gerbang mana pun:
lima namespace (`comments`, `newsletter`, `theming`, `seo`, `search`) menyandang
nama modul yang sudah ada dan berisi kode milik modul itu, dengan
`seo_distribution` bahkan merujuk ke ATAS ke `src/lib/seo/` lewat jalur yang
validator DAG tidak bisa lihat. Penyebabnya: kontrak modul tidak punya tempat
bagi kode presentasi/pengiriman, sehingga `src/lib/<nama-modul>/` menjadi
satu-satunya rumah yang tersedia.

ADR-0038 memutuskan definisinya: `src/lib` hanya infrastruktur teknis yang tidak
menyandang nama domain; composition root rute, glue middleware, dan skrip klien
browser milik modul tinggal di `src/modules/<m>/presentation/`. Lapisan tidak
dienumerasi di kode mana pun (tiga lapisan lain juga tidak), jadi tidak ada
mekanisme baru yang ditambahkan ke `module-contract.ts` — yang ditegakkan mesin
adalah gerbangnya, bukan penamaan lapisannya.

- Sepuluh berkas dipindah dengan `git mv` (riwayat terjaga). Pemindahan MURNI:
  tidak ada perubahan perilaku, API, migration, event, permission, atau registry
  (tetap 22 modul; `MODULE_CONTRACT_VERSION` tidak berubah).
- `modules:dag:check` diperluas: GAGAL bila sebuah namespace `src/lib/<x>/`
  bertabrakan nama dengan `moduleKey` — persis, atau lewat alias domain terdaftar
  (`seo` -> `seo_distribution`, `search` -> `site_search`), tanpanya dua dari lima
  kasus historis akan lolos. Pesan gagalnya menyebut modul pemilik + tujuan pindah.
- `tests/unit/lib-namespace-ownership.test.ts` MENYUNTIKKAN pelanggaran untuk
  membuktikan gerbangnya benar-benar menolak (termasuk memanggil `main()` skrip
  di atas pohon fixture dan memeriksa exit code 1) — bukan sekadar hijau setelah
  diperbaiki. Satu pengecualian tercatat: `src/lib/logging/` (primitif logger
  bebas database, dipakai ~139 berkas); tesnya membuktikan `logging` TERDETEKSI
  dan hanya disenyapkan oleh tabel pengecualian, bukan titik buta deteksi.
- `src/lib/files/` dan `src/lib/errors/` yang kosong (hanya `.gitkeep`) dihapus.

Refs #371

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(api): satu factory defineTenantRoute untuk pembukaan auth/tenant rute API

Pembukaan auth/tenant yang disalin ke 201 dari 260 rute (resolveAuthInputs →
cek tenant/token → getDatabaseClient → hashSessionToken → withTenant →
authorizeInTransaction → short-circuit auth.denied) kini hidup satu kali di
src/modules/_shared/tenant-route.ts.

- `workClass` WAJIB di tipe factory (tanpa default): 221 rute memakai
  "interactive" karena tidak ada yang meneruskan argumennya, bukan karena ada
  yang memutuskannya. Menghilangkannya sekarang gagal kompilasi.
- `unavailableBehavior` di-hardcode "response" dan sengaja tidak bisa
  dioverride — rute adalah pemanggil Response menurut konstruksi; "throw"
  hanya untuk pemanggil non-Response (#323), dan di sini justru akan mengubah
  503 terkendali jadi 500.
- Larangan Promise.all atas satu `tx` (#324) didokumentasikan di tipe `tx`
  yang diterima handler.
- `prepare` menampung parsing body/query, cek Idempotency-Key, dan request
  hashing — tetap sebelum koneksi diambil.

Modul data_lifecycle dimigrasi penuh (5 file rute, 6 handler) tanpa perubahan
perilaku; tes integrasi lamanya tetap hijau tanpa disunting. Gerbang baru
`bun run api:tenant-route:check` menolak rute BARU yang memanggil withTenant
langsung, dengan daftar NOT_YET_MIGRATED berisi 235 rute lama yang hanya boleh
menyusut (entri basi juga menggagalkan gerbang). Generator work-class kini
mengenali defineTenantRoute — tanpa itu rute yang dimigrasi justru HILANG dari
registry, dan menolak menulis "default" untuk rute factory tanpa literal.

Refs #370

* test(website-platform): axe atas daftar/pencarian/404 publik + pemeriksa tautan operator

Menutup dua celah #296 yang masih terbuka di dalam repo.

1. `tests/e2e/public-discovery-a11y.e2e.ts` — axe-core (WCAG 2.2 A/AA,
   gagal pada critical/serious) atas permukaan di ANTARA beranda dan
   artikel: halaman daftar `/news` + `/blog/{tenantCode}`, halaman
   pencarian blog (kosong dan berisi), keempat cabang render `/search`
   (belum ada query / terlalu pendek / tanpa hasil / ada hasil), dan
   dokumen 404 publik bersama — dalam EN dan ID, pada viewport desktop
   dan ponsel, ditambah pemeriksaan pencarian yang dioperasikan hanya
   dengan keyboard (fokus terlihat + Enter mengirim form).

   Tiga mekanisme locale berbeda dipakai di permukaan publik; spec ini
   memakai `default_locale` tenant (tenant kedua ber-locale `id`) dan
   `?locale=` pada `/search`, bukan cookie yang dipakai spec lain.
   Status HTTP di-assert SEBELUM axe agar route yang diam-diam 404 tidak
   dilaporkan bebas pelanggaran. Kedua gate dibuktikan bisa MERAH lewat
   kontrol negatif (img tanpa alt; outline/box-shadow none).

   Spec ini menulis ulang singleton `awcms_micro_setup_state`, jadi ia
   memegang advisory lock lintas-berkas `setup-state-ownership` selama
   hidupnya; seed unik per-run dan dibersihkan di `afterAll` agar aman
   terhadap retry Playwright.

2. `scripts/link-check.ts` + `bun run link:check -- --url=<url>` —
   pemeriksa tautan yang bisa dijalankan operator terhadap URL mana pun.
   Merayapi graf halaman yang dirender dari URL awal, ditambah direktif
   `Sitemap:` di `robots.txt` dan sitemap index/anak, lalu memverifikasi
   setiap anchor internal, `rel=canonical`, `rel=alternate hreflang`,
   tautan feed, dan pagination benar-benar terselesaikan (<400 setelah
   redirect). Laporan JSON ke stdout mengikuti idiom
   `scripts/edge-cache-verify.ts`; exit 0 bersih / 1 tautan rusak /
   2 usage error atau URL awal tak terjangkau — crawl yang tidak
   menjangkau apa pun tidak pernah dilaporkan hijau.

   Internal-vs-eksternal ditentukan oleh HOST, bukan origin penuh:
   terukur di app ini `<loc>` sitemap memakai `https://` sementara
   permalink di halaman yang sama memakai `http://`, sehingga
   perbandingan origin-ketat akan melewati separuh tautan situs sendiri
   lalu melaporkan hijau. `--site-origin=` untuk memeriksa deployment
   lewat alamat lain (host staging, IP internal di balik CDN).

Ekstraksi tautan memakai pemindai satu-lintasan yang melewati
komentar/CDATA/`<script>`/`<style>` (bukan rantai strip-lalu-rescan yang
memicu CodeQL js/incomplete-multi-character-sanitization), dan entitas
`&amp;` didekode TERAKHIR (js/double-escaping).

Refs #296

* refactor(pages): pecah empat halaman admin raksasa jadi markup + lapisan presentation

51 halaman `.astro` berbanding 16 komponen berarti praktis tidak ada lapisan
komponen: tiap halaman membangun ulang tabel, dialog, form, dan penanganan
errornya sendiri sebagai teks di dalam satu berkas. Gelombang pertama memecah
empat halaman yang sudah punya jaring pengaman Playwright, dengan tiga gerakan
mekanis per halaman — bukan penulisan ulang.

- Skrip inline `<script>` (1.263 baris) pindah ke
  `src/modules/<m>/presentation/*-client.ts` (ADR-0038) dan diimpor kembali
  lewat `<script>` agar tetap di-bundle Astro; `bun run build` membuktikan
  keempat bundle klien terbentuk. Begitu jadi `.ts` ia masuk `tsc` dan bisa
  di-unit-test.
- Frontmatter pindah ke `presentation/<page>-page-data.ts`: permission gating,
  `withTenant` read, builder daftar opsi, dan builder blob `clientStrings` —
  yang terakhir diketik dengan interface milik modul kliennya sendiri, jadi
  `tsc` gagal kalau kedua sisi melenceng.
- Stylesheet pindah ke `presentation/<page>.css`. CSS itu kini global, bukan
  scoped Astro: tiga selector element-level di `tenant/domains` dijangkarkan
  ulang ke `.domain-manager` agar tidak merembes ke topbar `AdminLayout`.

Komponen diekstrak hanya untuk pola yang DIUKUR muncul di lebih dari satu
halaman: `ClientJsonData` (35/51 halaman), `LoadErrorNotice` (38/51),
`TextField` (27/51), `SelectField` (31/51), `FieldHint` (18/51),
`CheckboxField`/`CheckboxGroup` (16/51). Ketiganya sengaja tanpa CSS —
halaman-halaman itu tidak sepakat soal spacing, jadi komponen memiliki markup
+ nama kelas dan stylesheet halaman tetap memiliki tampilannya. `DataTable`
dapat prop `columns`/`dataRole`; `ConfirmDialog` membuat label opsional, yang
sekaligus memperbaiki `admin/registrations.astro` — satu-satunya pemanggil yang
tidak mengirim keduanya dan merender dua tombol tanpa teks (`bun run typecheck`
= `tsc --noEmit`, yang tidak pernah membaca `.astro`).

Hasil terukur: access-users 1005->397, analytics 1123->380, security 1069->399,
tenant/domains 1045->371. Keempat spesifikasi E2E-nya lulus TANPA disunting
(6 spec dijalankan nyata terhadap server lokal + Postgres, plus
`admin-a11y-smoke`). 48 unit test baru menutup cabang penanganan error yang
selama ini tidak pernah dieksekusi tes apa pun.

Dua temuan ikutan saat memindahkan kode: `tenant/domains` memanggil
`withTenant` tanpa `unavailableBehavior: "throw"` (kelas insiden PR #323),
dan bentuk closure-assignment di frontmatter `security` menyempitkan `policy`
jadi `never` bagi setiap referensi template.

Gerbang anggaran baris sengaja TIDAK ditambahkan di sini (menyentuh `scripts/`
+ `package.json`); dicatat sebagai langkah lanjutan di doc 14.

Refs #372

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(quality): pasang gerbang analisis statis untuk lapisan .astro

Sebelum ini lapisan .astro (51 halaman + 16 komponen, ~19.2k baris skrip
browser inline) tidak diperiksa alat apa pun: `tsc` melewati ekstensi tak
dikenal secara diam-diam, `bun run lint` hanya `prettier --check`, dan
`@astrojs/check` belum terpasang.

Yang ditambahkan:

- `bun run typecheck:astro` = `astro check --minimumSeverity error`
  (satu-satunya gerbang yang mengetik-periksa frontmatter .astro DAN isi
  `<script>` inline). Masuk `bun run check` + langkah CI tersendiri.
- ESLint flat config (`eslint.config.mjs`) di dalam `bun run lint`:
  `no-floating-promises` + `no-misused-promises` (type-aware, .ts),
  `no-floating-promises` untuk frontmatter .astro, dan aturan sintaks
  (`js.configs.recommended` + `@typescript-eslint/no-unused-vars`) di
  dalam blok `<script>` yang diekstrak processor.
- `"DOM"` + `"DOM.Iterable"` di `compilerOptions.lib`.

Angka terukur pada 0a8c3ba (v1.1.0): `astro check` 34 error (7 berkas)
-> 0; ESLint 49 error (48 no-floating-promises + 1 no-misused-promises)
-> 0 dengan SATU pengecualian per-berkas eksplisit. Tidak ada
`--max-warnings` longgar, tidak ada `@ts-ignore`/`any`, tidak ada aturan
yang dimatikan global.

`no-unnecessary-condition` sengaja BELUM dinyalakan: 87 temuan di ~40
berkas (termasuk area yang sedang dikerjakan paralel) — didokumentasikan
sebagai hutang terukur di doc 07, bukan dilonggarkan diam-diam.

Bug NYATA yang ditemukan gerbang baru ini:

1. /admin/registrations mengirim NAMA peran, bukan UUID-nya. `role.id`
   tidak ada pada `RoleWithPermissions` (yang benar `role.roleId`);
   `<option>` tanpa atribut `value` membuat `select.value` mengembalikan
   teks opsi, sehingga approve-dengan-peran selalu mengirim roleIds yang
   tidak valid.
2. /admin/media merender badge "null B" untuk objek tanpa ukuran
   (`formatSize(bytes: number)` diberi `number | null`; `null < 1024`
   bernilai true).
3. /admin/reporting/projections memanggil `showBanner(..., "warning")`
   padahal union-nya hanya "success" | "error" DAN tidak ada CSS
   `[data-variant="warning"]` di mana pun — peringatan ketidakcocokan
   rekonsiliasi tampil tanpa gaya, tak terbedakan dari pesan netral.
4. Dua `<ConfirmDialog>` dirender tanpa prop wajib `confirmLabel`/
   `cancelLabel` (kosmetik: label ditimpa saat dialog dibuka).
5. 48 promise `Bun.serve(...).stop(true)` tanpa `await` di teardown test
   (kandidat penyebab flakiness port/timing).

Selain itu 26 dari 34 error `astro check` berasal dari satu pola:
`let x: T | null = null` yang ditulisi DARI DALAM callback `withTenant`
— analisis alur TypeScript tidak melihat penulisan itu sehingga `x`
menyempit jadi `never` di seluruh template. Diperbaiki dengan
mengembalikan nilai sebagai return value callback, yang otomatis
menuntut `{ unavailableBehavior: "throw" }` (AGENTS.md #8, kelas bug

Perubahan devDependency: `typescript` diturunkan `^7.0.2` -> pin eksak
`6.0.3`. TypeScript 7 adalah kompiler native yang tidak lagi mengekspor
API programatik, sehingga `astro check` menolak jalan dan
`typescript-eslint` (peer `<6.1.0`) tidak bisa dipakai sama sekali.
`tsc --noEmit` tetap nol error, hanya 0,8 s -> 6,9 s.

Refs #369

* fix(quality): karantina toolchain TS 6 agar root tetap TypeScript 7

Revisi atas commit sebelumnya: menurunkan `typescript` root ke 6.0.3
adalah arah yang salah. `tsc --noEmit` memeriksa 100% pohon `.ts`
(seluruh src/lib, src/modules, scripts, tests), sementara `astro check`
hanya menyentuh `.astro` — melemahkan gerbang yang lebih luas demi
tooling yang lebih sempit tidak sepadan.

Root dikembalikan ke `typescript ^7.0.2` (terbukti: `tsc --version` =>
7.0.2, `bun run typecheck` 0,8 s, 0 error). Toolchain yang masih terikat
API programatik TypeScript dikarantina di `tools/static-analysis/`
dengan package.json + lockfile + node_modules sendiri berisi
`typescript` 6.0.3 (pin eksak), `@astrojs/check`, `eslint`,
`typescript-eslint`, `eslint-plugin-astro`, `eslint-plugin-jsx-a11y`.
Bukan anggota workspace Bun, jadi TS 6 tidak ter-hoist ke root
(terbukti: `bun run static-analysis:versions` => root 7.0.2, quarantine
6.0.3).

`scripts/static-analysis.ts` menjalankan biner karantina dengan cwd =
root repo, sehingga masing-masing me-resolve TypeScript dari pohonnya
sendiri:

- `bun run typecheck:astro` -> astro-check (1370 berkas, 0 error)
- `bun run lint:eslint`     -> eslint (1372 berkas dipindai, 0 masalah)
- `bun run static-analysis:quarantine:check` -> GAGAL begitu
  `peerDependencies.typescript` toolchain yang terpasang menerima 7.x,
  jadi tidak ada yang perlu mengingat kapan workaround ini dicabut.
  Offline, deterministik, ikut `bun run check` + CI.

Asersi cakupan, bukan sekadar "scanned > 0". Varian `basePath` yang
disarankan literatur diukur me-lint TEPAT 1 berkas lalu exit 0 tanpa
pesan apa pun — "> 0" akan lolos. Runner-nya menghitung berkas sumber
nyata (1365) dan menolak hasil di bawah 90% darinya. Dibuktikan dengan
mutation test: config lumpuh -> "hanya memeriksa 1 berkas, padahal
pohon sumber punya 1365 (minimum 1228). Gerbang MATI, bukan hijau",
exit 1.

Konsekuensi karantina yang jujur dicatat: type-aware ESLint pada
frontmatter `.astro` menjadi mustahil, karena `astro-eslint-parser`
me-resolve `typescript` dari `process.cwd()` (= root = TS 7) saat
membangun program tipe. Nol temuan hilang (aturan itu memang 0 temuan),
dan `astro check` tetap mengetik-periksa frontmatter sepenuhnya. Blok
`<script>` tetap di-lint (aturan sintaks) — dibuktikan ulang dengan
mutation test setelah pemindahan.

Dua jebakan tambahan yang ditemukan saat memindahkan konfigurasi:
`src/**/*.ts` juga cocok dengan path virtual `x.astro/1_1.ts` sehingga
aturan type-aware ikut menempel di sana (butuh `ignores`), dan
`parserOptions` flat-config di-merge dangkal sehingga `project: null`
saja tidak mematikan `projectService: true` warisan blok sebelumnya.

Semua pekerjaan lain dipertahankan apa adanya: 5 bug nyata, daftar
pengecualian per-berkas, keputusan tidak mengaktifkan
`no-unnecessary-condition`, dan langkah CI.

Refs #369

* chore(quality): tegakkan gerbang tenant-route + sinkronkan skill dengan arsitektur baru

Gerbang `api:tenant-route:check` (Issue #370) sebelumnya bisa dijalankan
tapi tidak ditegakkan — agent yang membuatnya tidak boleh menyunting baris
`check` bersama. Sekarang terpasang, sehingga daftar `NOT_YET_MIGRATED`
benar-benar hanya bisa menyusut.

Tiga skill disinkronkan: `awcms-micro-new-endpoint` (defineTenantRoute
wajib untuk rute tenant-scoped baru), `awcms-micro-new-module` (lapisan
`presentation/` + batas `src/lib` per ADR-0038), `awcms-micro-ui-screen`
(pola halaman `.astro` tipis, angka nyata hasil #372).

Sekalian membuang dua klaim basi di `awcms-micro-new-endpoint`: topologi
"LAN-first" (dihapus ADR-0034/0036) dan pernyataan bahwa rute publik
tenant-scoped belum punya implementasi contoh. Skill tidak tercakup
`bun run check`, jadi keduanya bertahan sejak refactor lama.

Refs #369 #370 #371 #372

* fix(quality): perbaiki gerbang ESLint yang gagal karena ukuran laporan, lalu tangani 18 temuan yang tersembunyi di baliknya

Gerbang `lint:eslint` dari #369 tidak pernah benar-benar melaporkan isinya
di pohon terintegrasi. Dua sebab, keduanya membuatnya merah karena alasan
yang tidak ada hubungannya dengan kode:

1. `ignores` tidak memuat `.claude/**`, sehingga ESLint memindai seluruh
   worktree agent paralel BESERTA keluaran `dist/`-nya. Pola flat-config
   tanpa awalan `**/` bersifat root-anchored, jadi `dist/**` tidak
   menangkap `dist` bersarang. CI tidak punya worktree, jadi cacat ini
   hanya menggigit secara lokal — justru karena itu ia harus dipatok.
2. `Bun.spawnSync` MEMOTONG pipa stdout untuk laporan JSON sebesar ini
   (~350 KB), sehingga parse-nya meledak di tengah token dan gerbang
   melaporkan "JSON tidak valid". Laporan kini lewat `-o` ke berkas,
   sehingga ukuran laporan tidak bisa lagi menentukan vonis.

Setelah gerbangnya jujur, ia menemukan 18 `no-misused-promises` — semuanya
di berkas `presentation/` BARU, yaitu kode yang baru terlihat oleh ESLint
justru karena #372 memindahkannya keluar dari `.astro`. Tesis epik #373
membuktikan dirinya sendiri.

Semua 18 adalah `addEventListener("submit", async …)`: DOM menerima promise
yang tidak pernah dilihat siapa pun, jadi kegagalan menjadi unhandled
rejection dan pengguna tidak melihat apa-apa — kelas kegagalan senyap yang
sama dengan #359/#361. Ditangani lewat satu chokepoint `asyncHandler()`
di `admin-form-client.ts`; island komentar PUBLIK memakai kembarannya
secara lokal agar bundel publik tidak menyeret klien admin.

`i18n/messages.pot` diregenerasi (referensi baris bergeser karena kode
pindah berkas) dan `api:tenant-route:check` kini bagian dari `bun run check`.

Refs #369 #370 #371 #372

* fix(security): tangani temuan ronde review PR #374

Reviewer + security auditor berjalan atas PR ini. Nol CRITICAL, nol BLOCKER.
Yang diperbaiki di sini adalah temuan yang berada di berkas yang memang sudah
dibuka PR ini.

XSS laten — `ClientJsonData.astro` (muncul di KEDUA review). `JSON.stringify`
tidak meng-escape `<`, sehingga payload berisi `</script>` menutup data island
dan sisanya diparse sebagai HTML hidup. Nol call site eksploitabel hari ini
(semuanya string i18n milik repo), tapi komponen ini mengiklankan diri
menerima "payload JSON apa pun" dan 35 halaman admin adalah kandidat
pemakainya. Serializer dipindah ke `src/lib/ui/client-json-data.ts` agar bisa
diuji + 3 unit test dengan payload `</script>` sungguhan.

Kelas #323 di rute PUBLIK — tiga pemanggil non-`Response` di modul `theming`
memanggil `withTenant` tanpa `unavailableBehavior: "throw"`. Pada
`/theming/tokens.css` pool jenuh berarti `[object Response]` tersaji sebagai
CSS di setiap page load. Kini degradasi ke tema default / preview `null`.
Pra-ada, tapi PR ini menyapu kelas yang sama di `admin/tenant/domains` dan
melewatkan tiga ini di berkas yang ia sentuh sendiri.

Pengerasan gerbang, semuanya kelas "hijau tapi mati" yang PR ini justru
melawan:
- `validate-module-graph.ts` — `src/lib` tak terbaca kini MELEMPAR, bukan
  melaporkan pohon kosong yang lolos; lookup tabel pakai `Object.hasOwn`.
- `static-analysis.ts` — laporan tmp pakai `mkdtemp` (path lama bisa ditebak
  dan jadi target symlink lewat `-o` ESLint); pembersihan dipindah ke sebelum
  `fail()` karena `process.exit()` melewati `finally`.
- `api:tenant-route:check` jadi langkah CI eksplisit — CI menjalankan gerbang
  satu per satu, jadi memasangnya di `bun run check` saja tidak cukup.
- Daftar pengecualian ESLint dikosongkan; entrinya sudah basi di PR yang sama.
- `link-check.ts` — filter skema jadi ALLOW-list (deny-list lama melewatkan
  `file:`), userinfo dibuang dari laporan JSON.

Koreksi ADR-0038. Klaim "nol impor relatif lintas-modul di seluruh
src/modules" SALAH dan saya yang menulisnya. Pengukuran ulang menemukan ~80
impor semacam itu yang sudah ada jauh sebelum ADR ini. Yang benar: tidak ada
SIKLUS antar modul, dan yang dijaga hari ini adalah dependensi yang
DIDEKLARASIKAN — bukan impor tingkat berkas. Lubang itu kini tercatat sebagai
pekerjaan lanjutan, bukan diklaim selesai.

Refs #369 #370 #371 #372 #374

---------

Co-authored-by: AWCMS-Micro Security <security@awcms-micro>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant