| name | awcms-mini-module-management |
| description | Kelola/konsumsi sistem Module Management AWCMS-Mini (registry, validasi komposisi registry base, tenant lifecycle enable/disable, settings, permission sync/status, navigation, job registry, health/readiness). Gunakan saat menambah field descriptor baru (permissions/navigation/settings/jobs/health) di modul lain, saat menambah modul domain baru langsung di `src/modules/` dan perlu memverifikasi registry base tetap valid (`bun run modules:compose:check`), saat menyelidiki kenapa suatu modul terlihat degraded/orphaned, atau saat mengubah perilaku enable/disable/settings/health module_management sendiri. Sesuai src/modules/module-management/README.md, epic |
AWCMS-Mini — Module Management System
Ikuti src/modules/module-management/README.md (sumber kebenaran penuh
per issue #511-#521) dan docs/awcms-mini/10_template_kode_coding_standard.md
§Module contract. Skill ini merangkum pola yang tidak jelas dari
sekadar membaca satu file — dependency graph, urutan sync, semantik
merge settings, dan makna tiap sinyal health.
Kapan pakai skill ini vs awcms-mini-new-module
awcms-mini-new-module = cara scaffold modul baru (struktur folder,
descriptor minimal). Skill ini = cara kerja sistem yang mengelola
modul yang sudah terdaftar — enable/disable per tenant, settings,
permission sync, navigation, jobs, health. Pakai skill ini saat modulmu
sudah ada dan kamu perlu mendeklarasikan permissions/navigation/
settings/jobs di descriptornya, atau saat menyelidiki masalah di
sistem module management itu sendiri.
"Sync first" — aturan FK yang wajib dipahami
awcms_mini_tenant_modules, _module_settings, _module_health_checks
semua punya FK ke awcms_mini_modules.module_key. Mendaftarkan modul di
src/modules/index.ts tidak otomatis membuat baris registrynya.
Setiap mutasi tenant-scoped yang butuh baris registry ada
(enableTenantModule/disableTenantModule/updateModuleSettings/
runModuleHealthCheck) memanggil syncModuleDescriptors(tx) sendiri di
awal — jangan asumsikan operator sudah menjalankan
POST /api/v1/modules/sync manual lebih dulu. Bila menambah mutasi baru
dengan FK serupa, ikuti pola yang sama.
Konsekuensi: GET /api/v1/modules/{moduleKey}/health's sinyal
db_registry_synced bisa fail di instance yang baru dimigrasikan
(belum pernah ada mutasi tenant-scoped apa pun) — ini bukan bug,
laporan yang jujur. POST .../health/check men-sync duluan sebagai efek
samping menulis riwayat, jadi bisa menunjukkan hasil pass untuk sinyal
yang sama di momen yang sama — asimetri yang disengaja, didokumentasikan
di README modul.
Dependency graph (enable/disable)
Graph selalu dibaca dari listModules() (code), tidak pernah
dari awcms_mini_module_dependencies (cache hasil sync terakhir — bisa
basi). Kode error dari domain/tenant-module-lifecycle.ts:
| Kode | Kapan |
|---|
MODULE_NOT_FOUND | Key tidak terdaftar / dinonaktifkan global (code) |
MODULE_ALREADY_ENABLED/_DISABLED | Tidak ada perubahan state |
MODULE_DEPENDENCY_MISSING | Dependency tidak terdaftar sama sekali |
MODULE_DEPENDENCY_DISABLED | Dependency nonaktif (global atau tenant ini) |
MODULE_REVERSE_DEPENDENCY_ACTIVE | Modul lain yang aktif masih bergantung padanya |
MODULE_DEPENDENCY_CYCLE | Circular dependency di graph |
MODULE_VERSION_INCOMPATIBLE | minAppVersion modul > versi app saat ini |
CORE_MODULE_CANNOT_BE_DISABLED | isCore: true — tidak bisa dinonaktifkan, bukan bug |
Modul isCore: true (saat ini hanya module_management sendiri) tidak
bisa dinonaktifkan — ini pencegah admin lockout utama: kemampuan
mengelola modul lain tidak pernah hilang.
Registry-wide DAG validator (Issue #680, epic #679) — beda dari hasDependencyCycle
hasDependencyCycle di atas hanya pernah dipanggil untuk SATU modul (yang
sedang dicoba di-enable, evaluateModuleEnable — lihat tenant-module-lifecycle.ts:138)
— tidak pernah dipakai untuk memeriksa "apakah SELURUH registry sudah
DAG yang valid". Celah inilah yang membuat tenant_admin/
profile_identity/identity_access sempat punya cycle 3-node nyata di
dependencies masing-masing (tenant_admin -> profile_identity -> tenant_admin, dst) selama registry-nya tidak pernah diiterasi
menyeluruh — padahal hasDependencyCycle SUDAH akan menolaknya kalau
ada yang mencoba meng-enable salah satu dari ketiganya lewat jalur
normal.
domain/module-dependency-graph.ts's validateModuleDependencyGraph(listModules())
adalah pemeriksaan menyeluruh itu — mendeteksi EMPAT masalah berbeda
sekaligus (tidak berhenti di yang pertama): self_dependency,
duplicate_dependency, missing_dependency, dan cycle
(langsung/tidak langsung, algoritma Kahn menyeluruh, bukan DFS
satu-titik). Dipanggil dari:
bun run modules:dag:check (scripts/validate-module-graph.ts) —
disisipkan ke bun run check tepat setelah api:spec:check.
bun run modules:sync (scripts/modules-sync.ts) — menolak sync ke DB
bila graph rusak, SEBELUM baris apa pun tersentuh. Sejak Issue #697 (epic
#679), script ini dibangun di atas shared worker runner
src/lib/jobs/job-runner.ts (advisory lock, --dry-run via
planModuleSync, JSON telemetry) — lihat
docs/awcms-mini/deployment-profiles.md §Shared worker runner; perilaku
syncModuleDescriptors sendiri TIDAK berubah.
Paritas import vs dependencies — gerbang yang BERBEDA dari DAG di atas
bun run modules:imports:check (scripts/validate-module-imports.ts).
modules:dag:check divalidasi dari listModules(), yaitu array dependencies
tulisan tangan. Karena itu ia secara struktural TIDAK MUNGKIN menyadari satu
hal yang justru paling gampang membusuk: edge import yang ada di KODE tapi tidak
pernah dideklarasikan. Sebuah modul bisa menumbuhkan dependency runtime ke modul
lain dan semua gate lama tetap hijau, karena tidak ada yang pernah membandingkan
keduanya.
Ini bukan pembukuan teoretis. dependencies bermakna operasional (#845/PR#855):
menentukan protected-module, preset deployment profile, dan reverse-dependency
guard yang menolak men-disable modul yang masih dibutuhkan. Edge yang TIDAK
dideklarasikan berarti platform boleh men-disable B sementara A masih
memanggilnya saat runtime — gagalnya baru terlihat di produksi, pada tenant yang
kebetulan men-disable modul yang tepat.
Tiga keputusan desainnya, jangan "diperbaiki" tanpa membaca ini:
- Hanya import RUNTIME. Deteksi memakai
Bun.Transpiler.scanImports, yang
MENGHAPUS import type sebelum melapor — itu semantik yang diinginkan, bukan
keterbatasan. dependencies mengatur pengaktifan runtime, jadi import
type-only (yang lenyap saat build dan tak bisa memanggil apa pun) TIDAK boleh
dipaksa mendeklarasikan dependency runtime. Memaksanya akan mendorong orang
menambahkan edge PALSU hanya demi menyenangkan gate, dan edge itu lalu
mengubah perilaku protected-module/preset/reverse-dep yang sungguhan.
- Parse betulan, bukan regex. Sintaks import punya terlalu banyak bentuk
(named multi-baris, side-effect,
import() dinamis, export ... from);
daftar pola pasti melewatkan sebagian, dan gate dengan titik buta diam-diam
terbaca "sudah diverifikasi" padahal membuktikan sedikit.
- Nama direktori BUKAN module key. Peta direktori→key dibangun dengan
meng-import
module.ts tiap direktori dan membaca key deskriptornya, karena
keduanya memang menyimpang (workflow-approval/ ber-key workflow, lihat
§Module key vs nama direktori). Transform replace("-","_") akan salah
meresolusi modul itu lalu melewatkan atau mengarang pelanggaran.
Arah _shared sengaja satu arah: import KE _shared selalu boleh (itu jalur
capability port ADR-0011/ADR-0013 yang disahkan), sedangkan _shared yang
meng-import modul konkret dilaporkan sebagai pelanggaran TERSENDIRI — persis
bentuk yang harus dibongkar di #859.
Saat gate ini ditulis invariannya sudah bersih (0 edge tak-terdeklarasi dari 241
edge import lintas-modul). Justru itu alasannya murah dipasang SEKARANG: ia
mengunci keadaan baik, bukan datang membawa backlog.
Fix nyata untuk cycle historis (Issue #680): tenant_admin.dependencies
diubah dari ["profile_identity", "identity_access"] menjadi [] —
profile_identity/identity_access's array masing-masing SUDAH benar
sejak awal (profile_identity: ["tenant_admin"],
identity_access: ["tenant_admin", "profile_identity"]); satu-satunya
edge yang salah arah adalah tenant_admin balik menunjuk keduanya.
Alasan historis edge itu ada: tenant_admin's one-time setup wizard
(POST /api/v1/setup/initialize) menulis baris ke tabel
profile_identity/identity_access DALAM transaksi yang sama — itu
kebutuhan saat-dipanggil (call-time), bukan "tenant_admin tidak bisa
berfungsi sama sekali tanpa keduanya" (static dependency yang salah).
Orkestrasi itu sekarang jadi fungsi composition-root eksplisit,
application/platform-bootstrap.ts's bootstrapPlatformTenant, dipanggil
langsung oleh route handler — bukan lewat dependencies array. Jangan
kembalikan pola lama ini kalau butuh orkestrasi lintas-modul serupa di
masa depan — buat composition-root function baru, jangan tambah edge
dependencies untuk menjustifikasi urutan panggilan satu-kali.
resolveProtectedModuleKeys's (module-presets.ts) hasil closure untuk
module_management — {module_management, tenant_admin, identity_access, profile_identity} — TIDAK berubah meski edge tenant_admin dihapus,
karena closure dihitung lewat identity_access -> profile_identity -> tenant_admin (masih transitif sama), bukan lewat edge tenant_admin yang
dihapus. Verifikasi ini lewat test yang sudah ada
(tests/unit/module-presets.test.ts's "real registry's protected set is
exactly module_management's own dependency closure").
capabilities — hubungan source-level, BEDA dari dependencies (Issue #681, epic #679)
ModuleDescriptor punya field opsional baru, capabilities?: {provides?: string[]; consumes?: {capability, providedBy, optional?}[]}
(_shared/module-contract.ts). Ini BUKAN bagian dari dependency-graph
lifecycle di atas — dependencies tetap satu-satunya field yang dibaca
hasDependencyCycle/validateModuleDependencyGraph/evaluateModuleEnable/
evaluateModuleDisable. capabilities murni mendokumentasikan hubungan
IMPORT SOURCE-LEVEL lewat pola ports-and-adapters (_shared/ports/*.ts)
— lihat ADR-0011 dan skill awcms-mini-news-portal's §681 untuk contoh
nyata (blog_content/news_portal). Modul yang butuh kapabilitas dari
modul lain TIDAK PERNAH meng-import application/domain modul itu
langsung — hanya port interface (_shared/ports/) di layer
application/domain, dengan adapter konkret disuntikkan pemanggil
(route handler = composition root). optional: true di consumes
berarti fitur pemanggil degradasi aman (bukan error) kalau kapabilitas
itu resolve ke "tidak berlaku" untuk suatu tenant — bukan berarti kode
bisa jalan tanpa modul lain ter-compile (ini monolith, semua source
selalu ikut ter-bundle).
Dua varian composition-root sudah ada di repo ini — pilih sesuai
taruhan keamanan fitur, bukan template tunggal. Varian #1
(blog_content konsumsi NewsMediaPort dari news_portal, Issue #681):
route handler SELALU inject adapter konkret, TANPA cek enable/disable
tenant di call site — port itu sendiri yang didesain fail-closed/no-op
aman untuk setiap kasus "tidak berlaku". Varian #2 (identity_access
konsumsi BusinessScopeHierarchyPort dari organization_structure,
Issue #746/#749/#786): composition root (POST /api/v1/identity/ business-scope/assignments's buildHierarchyPort) SECARA EKSPLISIT
memanggil resolveModuleEnabled(tx, tenantId, "organization_structure")
lebih dulu — hanya mencoba adapter nyata modul itu saat aktif untuk
tenant tsb, jatuh ke adapter default modul pengonsumsi kalau tidak. Pilih
varian #2 (gate eksplisit) ketika kapabilitas yang dikonsumsi menentukan
keputusan otorisasi/keamanan (di sini: apakah sebuah scope reference
valid sebelum SoD dievaluasi) — men-degradasi "aman" secara implisit
lewat port semata (varian #1) berisiko diam-diam mengonsultasikan data
milik modul yang justru sudah dinonaktifkan tenant. Kedua varian tetap
sama-sama TIDAK PERNAH meng-import application/domain modul lain
langsung dari modul pengonsumsi — hanya lewat port + composition root,
lihat identity-access/README.md dan organization-structure/README.md
§BusinessScopeHierarchyPort untuk detail varian #2, dan
tests/integration/business-scope-organization-structure-wiring. integration.test.ts untuk buktinya end-to-end.
Baca status tenant-enabled: plural vs singular
fetchTenantModuleEntries(tx, tenantId) (semua modul terdaftar) vs
fetchTenantModuleEntry(tx, tenantId, moduleKey) (satu modul,
SELECT-nya di-filter module_key langsung, bukan filter di memori).
Pakai yang singular kalau consumer-mu cuma butuh status satu modul
spesifik (terutama di gate publik/anonim — narrower read surface untuk
kode yang tidak authenticated), seperti blog-content's
public-news-tenant-resolution.ts. Pakai yang plural kalau memang
butuh daftar lengkap (endpoint GET /api/v1/tenant/modules, tenant module
presets, tenant-module matrix UI). Keduanya punya semantik
opt-out-by-default yang sama (tidak ada row awcms_mini_tenant_modules
→ tenantEnabled: true). Detail lengkap:
module-management/README.md §Tenant module lifecycle, skill
awcms-mini-tenant-domain-routing §Belum ada (Sudah diperbaiki).
Tenant module presets (Issue #565, epic #555)
domain/module-presets.ts + application/module-presets.ts
(applyModulePreset) — set state modul tenant sekaligus ke sebuah
"profil" (online_website, news_portal, saas_online, pos_lan,
minimal), 100% reuse evaluateModuleEnable/evaluateModuleDisable/
enableTenantModule/disableTenantModule di atas — tidak pernah
menulis awcms_mini_tenant_modules langsung. Preset menerapkan enable
DAN disable (bukan cuma enable) — modul yang tidak ada di daftar preset
dan bukan "protected" (isCore + closure transitif dependency-nya,
dihitung dinamis lewat resolveProtectedModuleKeys) akan di-disable,
leaves-first, skip (bukan force) untuk modul yang masih dibutuhkan modul
lain yang tetap enabled. Idempotent (re-apply = plan kosong). Baru
service layer — belum ada endpoint API/UI (scope Issue #566). Detail
lengkap: module-management/README.md §Tenant module presets, skill
awcms-mini-tenant-domain-routing §Tenant module presets (Issue #565).
Settings — merge dangkal, bukan replace
PATCH .../settings men-merge dangkal body ke tenantOverride yang
ada ({ ...before, ...patch }) — key yang tidak disebut tetap tidak
berubah. Berbeda dari PATCH /api/v1/settings's featureFlags (replace
utuh field itu) karena di sini seluruh body request adalah resource
settings-nya, bukan satu field bernama di resource lain. Key yang
menyerupai secret (daftar sama _shared/redaction.ts's REDACTION_KEYS,
termasuk credential) ditolak saat request (400 SETTINGS_SENSITIVE_KEY_REJECTED), tidak pernah disimpan lalu di-redact
saat dibaca. Value berbentuk credential juga ditolak walau key-nya
tidak mencurigakan (_shared/redaction.ts's findSecretShapedValues —
JWT, blok PEM private key, AWS access key id, header Bearer/Basic
mentah, connection string ber-user:pass@; sengaja konservatif supaya
label/URL/flag biasa tidak pernah salah tertolak) — 400 SETTINGS_SECRET_SHAPED_VALUE_REJECTED, pesan error hanya menyebut path
key, tidak pernah value-nya. Berlaku otomatis untuk semua modul yang
pakai validateModuleSettingsPatch, tanpa perlu ubah route/modul
masing-masing.
Permission sync status — jangan auto-fix orphaned
GET /api/v1/modules/{moduleKey}/permissions (Issue #517) melaporkan
synced/missing/orphaned/mismatched_description — read-only,
tidak pernah menulis ke awcms_mini_permissions. 17 modul (dari 23)
sudah mendeklarasikan permissions di descriptornya — module_management,
blog_content (sejak Issue #543, 39-entry array), idn_admin_regions,
news_portal, social_publishing, tenant_domain, visitor_analytics,
profile_identity, reporting, workflow_approval, plus 7 modul
platform-evolution epic #738: data_exchange, data_lifecycle,
document_infrastructure, domain_event_runtime, integration_hub,
organization_structure, reference_data. 6 modul lain (email,
form-drafts, identity-access, logging, sync-storage, tenant-admin) punya
permission seed nyata (dari migration masing-masing) tapi belum
ditambahkan ke descriptor — jadi permission mereka legitimately muncul
orphaned hari ini, bukan insiden. Jangan hapus baris awcms_mini_permissions
berdasarkan laporan ini tanpa keputusan admin eksplisit.
Health check — GET pasif, POST eksplisit
GET .../health = sinyal generik murah saja (registry synced, migrasi
diterapkan, permission/jobs/OpenAPI/AsyncAPI terdokumentasi, settings
valid) — tidak pernah memanggil provider eksternal, aman dipanggil
berulang. POST .../health/check = sinyal sama plus live check ke
provider bila modul punya satu (email saat ini, lewat
resolveEmailProvider().healthCheck() yang sudah timeout-bounded sejak
Issue #495) — dan menulis riwayat ke awcms_mini_module_health_checks.
Menambah provider check baru untuk modul lain: ikuti pola yang sama
(hanya di POST, bounded/non-throwing, detail selalu string generik
tetap — tidak pernah pesan error mentah).
Job registry — dokumentasi murni
ModuleDescriptor.jobs tidak pernah jadi permukaan eksekusi command
dari web — hanya metadata (command, purpose, recommendedSchedule,
environmentNotes, safeInOfflineLan). Jangan tambah endpoint yang
menjalankan command dari sini; bila eksekusi job dari UI benar-benar
dibutuhkan suatu saat, itu harus fitur terpisah yang dibatasi ketat
(security note eksplisit epic #510).
Verifikasi
tests/module-management-*.test.ts (domain, unit, per Issue) dan
tests/integration/module-*.integration.test.ts (API+RLS+audit
end-to-end, real Postgres) — jalankan bun test dengan DATABASE_URL
sebelum PR yang menyentuh sistem ini dianggap selesai (bun run check
tanpa DATABASE_URL melewatkan semua test integration secara diam-diam).
Skill terkait
awcms-mini-new-module (scaffold modul baru, termasuk field descriptor
ini), awcms-mini-abac-guard (guard bersama yang juga menegakkan
403 MODULE_DISABLED), awcms-mini-sensitive-data/redaction
(REDACTION_KEYS yang dipakai validasi settings), awcms-mini-audit-log
(pola audit tenant_module_enabled/_disabled/settings_updated/health_checked).
Kebijakan admission modul (Issue #696)
docs/awcms-mini/21_module_admission_governance.md mendefinisikan
kategori modul (Core/System/Official Optional Module/Derived Application/
External Integration), kriteria admission, aturan dependency required vs
optional (§5, melengkapi capabilities di atas), ekspektasi kompatibilitas
offline/LAN vs full-online-only, dan pemetaan 23 modul terdaftar saat ini
(src/modules/index.ts's baseModules, termasuk 7 modul platform-evolution
epic #738: data_lifecycle, domain_event_runtime, organization_structure,
document_infrastructure, data_exchange, integration_hub, reference_data)
ke kategori tersebut (termasuk catatan remediasi field type/isCore/
maintainers yang belum konsisten diisi — lihat doc 21 §8). Baca dokumen
itu sebelum mengusulkan modul baru atau mengubah kategori/status lifecycle
modul yang sudah ada.
Contoh keputusan arsitektur — batas control-plane vs tenant-plane (SaaS Control Plane, ADR-0022, epic #868)
Preseden konkret cara sebuah kluster modul baru diadmisi TANPA merusak
boundary yang sudah ada — pakai sebagai template saat mengerjakan issue
#870–#881 (atau menilai apakah sebuah issue melanggar boundary):
- Placement: tujuh modul (
service_catalog, tenant_entitlement,
tenant_provisioning, tenant_lifecycle, usage_metering,
subscription_billing, payment_gateway) = Official Optional Business
Foundation in-repo, default-disabled (bukan repo terpisah). ADR-0022
meng-amend placement ADR-0013 §1 (yang dulu menaruh SaaS "di luar
base") lewat catatan bertanggal + ADR baru — JANGAN tulis ulang badan ADR
Accepted.
- Arah dependency: control-plane boleh depend Core/System; base/core/
modul bisnis TIDAK PERNAH depend ke logika SaaS. Satu-satunya jalur
tenant-plane → control-plane adalah kontrak capability
effective_entitlement read-only — bukan FK/import/table-write.
- JUJUR soal gate: policy boundary ini BELUM ter-gate untuk 7 modul SaaS.
modules:dag:check hanya memvalidasi graf dependency deklaratif
(dependencies/capabilities di module.ts), bukan policy
"tenant-plane hanya boleh konsumsi read-only entitlement port / no
shared-table-write". tests/unit/module-boundary.test.ts yang ada saat ini
hard-coded ke pasangan blog_content ↔ news_portal dan tidak
memeriksa ketujuh modul control-plane sama sekali. Artinya: mengandalkan
kedua check itu untuk meloloskan review #870–#881 adalah jebakan
[[validator-exists-but-unwired-critical-pattern]] / [[cycle-detector-fed-incomplete-graph]]
— import/FK/table-write langsung ke internal SaaS bisa lolos diam-diam.
Wave-1 (issue modul control-plane pertama, #870/#871) WAJIB menambah/
memperluas module-boundary.test.ts (atau gate setara) untuk menegakkan
read-only-entitlement-port + no-shared-table-write bagi 7 modul —
sesuai Consequences ADR-0022. Sampai gate itu mendarat, penegakan boundary
tujuh modul ini bersandar review manual + checklist, bukan mesin — jangan
klaim sudah tergate.
- Trust: platform/operator role bukan
BYPASSRLS (akses
lintas-tenant tetap lewat RLS + permission eksplisit); support access
cross-tenant reason/time-bound/audited; secret provider hanya di
process.env, tak pernah di tabel tenant-readable.
- Fail-safe: downgrade/suspend tidak pernah
DELETE data tenant
(ubah state + gate); provider di luar transaksi (outbox + webhook signed
inbox integration_hub, retry/DLQ, reconciliation); billing SaaS ≠
general ledger/AR-AP/tax (ADR-0013 §3, ADR-0020).
- "Default-disabled" masih gap runtime: default hari ini = enabled
(
tenant-module-lifecycle.ts). ADR-0022 §7 mewajibkan #870–#874
menutupnya (flag defaultTenantState atau aktivasi lewat preset/
entitlement) sebelum merge — verifikasi ini saat review modul SaaS.
Validasi komposisi registry base (ADR-0024, gate modules:compose:check)
Pertanyaan BERBEDA dari admission (§ di atas, yang mengatur "modul apa
boleh masuk registry ini"): apakah registry base — termasuk setiap modul
domain baru yang ditambahkan LANGSUNG ke src/modules/ — tetap membentuk
komposisi yang valid secara menyeluruh. Keluarga AWCMS dipakai LANGSUNG
sebagai template (ADR-0024, men-supersede ADR-0013/0014/0015): tidak ada
lagi repo aplikasi-turunan terpisah, seam application-registry.ts,
namespace migration turunan 900–999, manifest extension.manifest.json,
atau gerbang extension:check — semua permukaan itu sudah dihapus.
Modul domain baru (termasuk ekstensi ERP dan modul konten/website) hidup
di src/modules/ dan terdaftar di src/modules/index.ts lewat
listModules()/listBaseModules().
Mesin validasinya src/modules/module-management/domain/ module-composition.ts — validateComposedModuleRegistry(registry) /
composeModuleRegistry(registry) / buildComposedModuleInventory(registry),
semuanya menerima readonly ModuleDescriptor[] (registry base sendiri,
listModules()). Dipanggil EKSPLISIT oleh gate, tidak pernah oleh
index.ts sendiri — pola identik dengan validateModuleDependencyGraph/
modules:dag:check di atas, sengaja dipakai ulang bukan diduplikasi.
validateComposedModuleRegistry() melaporkan SETIAP masalah dalam
satu pass (tidak berhenti di yang pertama), menggabungkan empat isu DAG
(self_dependency, duplicate_dependency, missing_dependency,
cycle) dengan isu komposisi tambahan: duplicate_module_key,
capability_provider_conflict, capability_provider_missing,
deployment_profile_incompatible, navigation_path_conflict, dan
invalid_job_descriptor. Semuanya invariant base yang WAJIB tetap
benar saat modul domain baru ditambahkan langsung ke registry —
duplicate module key, capability binding (ports-and-adapters, § di
atas), profil deployment antar-dependency yang kompatibel, path
navigasi yang tak bentrok, dan job descriptor yang valid. Detail
lengkap di module-composition.ts's file header.
bun run modules:compose:check
(scripts/validate-module-composition.ts) menjalankan validator itu
terhadap listModules() dan gagal bila ada isu — wired ke bun run check.
bun run modules:composition:inventory:generate/:check —
snapshot JSON deterministik registry
(docs/awcms-mini/module-composition-inventory.json) untuk bukti
CI/rilis, juga wired ke bun run check. Regenerate setiap menambah/
mengubah modul.
- Fixture referensi:
tests/fixtures/example-domain-modules/ (modul
domain contoh in-repo — example-crm, example-loyalty,
example-erp-extension; bun test tests/unit/module-composition-fixture.test.ts) — contoh nyata yang bisa
dijalankan, bukan sekadar dokumentasi naratif.
MODULE_CONTRACT_VERSION kini 2.1.0. 2.0.0 = MAJOR (tipe kontrak
khusus jalur-turunan yang dulu diekspor — ApplicationModuleRegistry/
ModuleMigrationNamespace, mergeModuleRegistries, dan konsep namespace
migration 900–999 — dihapus; tidak ada field ModuleDescriptor yang berubah,
setiap module.ts tetap valid). 2.1.0 = MINOR aditif (#930): field opsional
serviceLevelObjectives?: ServiceLevelObjectiveDescriptor[].
Keluarga descriptor kontribusi-modul saat ini (pola sama: modul
mendeklarasikan array-nya sendiri, satu engine pusat membaca listModules(),
satu gate memvalidasi): permissions · navigation · jobs · settings ·
health · capabilities · dataLifecycle (#745) · sodRules (#746) ·
dataExchange (#752) · referenceData (#750) · reportingProjections (#753)
· defaultTenantState + serviceCatalog (#870/#874) ·
serviceLevelObjectives (#930).
Menambah keluarga baru = bump MINOR + tulis validator murni di modul
pemiliknya + gate bun run <x>:check + daftarkan step-nya di
.github/workflows/ci.yml (ada test yang gagal bila check punya langkah
yang tidak dijalankan CI).
Detail keputusan lengkap: docs/adr/0024-awcms-family-direct-use-templates-and-derived-pathway-removal.md.