Skip to content

Derivation Engine ​

Updated: 2026-10-08 · Status: Draft

Draft: isi di bawah kondisi kode engine/derive.ts hari ini.

1. Layer 0 Otomatis ​

Dari Entity manifest → Table, Form, Page detail, tanpa spec eksplisit — mewujudkan kontrak "Derivasi Otomatis" di ../../spec/frontend/06-page-kinds.md §9. Tipe output derivasi sama persis dengan tipe manifest hasil YAML, sehingga kind renderer tidak tahu bedanya derived vs authored.

Kesamaan tipe itu punya konsekuensi yang mudah terlewat: provenance tidak bisa dibaca dari nilainya. Di mana renderer memperlakukan kedua jalur berbeda — seperti window kolom Table (§2) — asalnya harus dibawa explisit, bukan ditebak dari bentuk hasil merge.

2. Aturan Derivasi ​

Table (deriveTable → deriveTableColumns): kolom = field non-child non-computed, semuanya ikut masuk spec.columns (tidak ada yang dibuang), diurutkan dengan sort stabil menurut tier ../../spec/frontend/06-page-kinds.md §3.1: natural_key → label_field (yang server resolve sebagai display_field → natural key → name/title/number → id; internal/ui/meta.golabelField) → state_machine.field → field bernama literal transaction_date → sisanya urutan deklarasi. Kolom belongs_to ditulis sebagai dot-path <alias>.name supaya sel menunjukkan nama record terkait, bukan UUID.

Per kolom: label = title field → nama di-humanise (fieldLabel); sortable = false untuk kolom relasi (server mengurutkan UUID-nya, dan ?sort=<relasi>.name ditolak 422), selain itu true untuk tipe string/integer/decimal/percent/date/datetime/enum/boolean; widget = badge untuk enum atau field bernama doc_status, badge juga untuk field ber-options tunggal (set dibiarkan teks), boolean untuk boolean, image untuk file/attachment yang storage-nya menerima gambar; format = relative (datetime), date (date), percent (percent), currency (money). page_size: 25; search: true bila ada field string non-enum; default_sort: -created_at hanya bila field created_at ada (kalau tidak, tak ada default sort). Row action beda per characteristic: summary → view saja; reference → view+edit tanpa delete; lainnya → view+edit+delete dengan pesan confirm hardcoded — ditambah aksi kustom yang punya ui hint (builtin dilewati).

Window kolom & row-expand. Renderer menampilkan DERIVED_TABLE_VISIBLE_COLUMNS (= 8) kolom pertama inline dan sisanya lewat row expand — bukan dibuang, seperti sebelumnya diklaim di §5. Pembedanya adalah asal kolom, dan itu tidak bisa dibaca dari hasil merge ({ ...derived, ...authored.spec } identik di kedua kasus), jadi TableRenderer membawa columnsAuthored dari memo resolusi spec dan menyerahkannya ke helper murni tableColumnWindow(columns, authored): daftar authored dirender penuh (15 dideklarasikan → 15 dirender), daftar derived yang di-window.

Form (deriveForm): satu section, semua field editable non-computed (mode create mengecualikan computed saja). Heuristik render: >12 field ATAU punya child field ber-storage: table → separate_page; >5 field → drawer; selain itu → modal. (Field child ber-storage: jsonbtidak ikut dihitung sebagai alasan separate_page — beda dari niat awal yang menghitung "punya child table" secara generik.)

Input action/transisi (resolveActionInputs, toFieldDescriptor di src/lib/actionParams.ts): bukan derivasi dari Entity, melainkan resolusi deklarasi — input yang dibaca dari params.inputs/inputs_from pada transisi/action, lalu disesuaikan ke bentuk Field supaya FormFieldWidget/buildZodField yang sama bisa menggambarnya. Karena itu tidak ada kosakata widget kedua yang perlu dijaga sinkron dengan widgets/catalog.tsx. Presedensi: params transisi → params action → input_sets entity; container dari params.render.mode → jumlah input (≤5 modal, 5–12 drawer, >12 separate_page, sama seperti deriveForm). Dipakai satu komponen (src/shell/ActionInputDialog.tsx) oleh DetailPage, Table (baris + massal, satu payload untuk seluruh seleksi), dan Kanban; lihat ../../spec/frontend/06-page-kinds.md §2.0.

Caption & help field (entityFieldLabel, entityFieldHelp, withEntityFieldDefaults, withEntityColumnLabels): caption memakai label manifest → title field Entity → nama field yang di-humanise; help memakai help manifest → description field Entity → tanpa elemen. Presedensi ini wajib sama di jalur derivasi dan authored, sesuai ../../spec/frontend/06-page-kinds.md §2. Dulu hanya fieldLabel() (jalur derivasi) yang membaca title entity; jalur authored memakai field.label ?? field.name, sehingga mendeklarasikan kind: Form/Table menurunkan seluruh caption menjadi nama mentah — 111 dari 161 field form authored di examples/ tidak menulis label:, jadi ini jalur umum, bukan kasus tepi. resolveForm()/resolveTable() kini meng-enrich manifest authored (mengembalikan salinan, bukan memutasi entry bundle yang dibagi antar render).

Sisi help-nya punya riwayat yang sama persis: formField() (jalur derivasi) membaca field.description, sedangkan form authored tidak pernah lewat sana — jadi description entity hilang begitu entity-nya punya Form, dan penulis menyalinnya manual (promo-form.yaml dan promo/entity.yaml memuat kalimat yang identik). withEntityFieldDefaults() kini mengisi label dan help sekaligus, plus sections[0].description dari entity.description — yang terakhir adalah subtitle drawer/dialog (shell/OverlayHost.tsx), yang tanpa itu jatuh ke teks Inggris generik. WizardFormStep dan jalur WizardRenderer untuk steps[].fields membaca Form langsung dari bundle (useMetaStore.getForm) sehingga melewati resolveForm() — keduanya memanggil resolver yang sama.

Renderer merender help hanya di permukaan input (Form create/edit + langkah Wizard, di bawah kontrol). Mode view dan permukaan baca-saja (Table, Listing, detail Page, Kanban, ChildTable) tidak merendernya — lihat §2 spec di atas.

Menu: fungsi deriveMenuItems() ada di kode tapi tidak dipanggil siapa pun — lihat 01-architecture.md §5. Menu surface app murni dari bundle.menu (resolved server-side dari App.spec.menu) tanpa fallback derived untuk entity yang belum masuk menu manapun; menu surface _admin dibangun terpisah, inline, di Sidebar.tsx (grouped per module).

Lifecycle buttons (engine/lifecycle.ts): tipe Lifecycle di kode hanya punya dua nilai — plain_crud dan two_step_autosave (bukan empat pola yang sempat didesain). getLifecycle() jatuh ke two_step_autosave untuk apa pun selain plain_crud. Pola "2-step manual" dan "1-step create-submit" dari ../../spec/frontend/06-page-kinds.md §2.1 belum jadi cabang lifecycle terpisah di renderer ini — yang ada cuma flag has_quick_submit sebagai tambahan di dalam two_step_autosave, bukan pola independen dengan UI sendiri.

3. Override ​

Presedensi authored > derived, tapi Tabel dan Form lewat jalur yang berbeda — dan itu bukan detail: setiap jalur presedensi yang ditulis dua kali adalah kesempatan keduanya menyimpang.

Table. Jalur yang benar-benar dipakai adalah merge inline di TableRenderer: cari entry bundle dengan t.spec.entity === "<module>.<entity.name>" (atau entity.name saja), lalu { ...deriveTable(entity), ...authored.spec } dengan columns dan row_actions authored menang bila non-kosong; kolom authored yang tidak menulis label mewarisi title entity (withEntityColumnLabels). Jalur ini sekaligus penentu columnsAuthored yang membedakan window derived vs authored (§2). resolveTable() di engine/derive.ts bukan jalur itu: ia memakai predicate lain (authoredTables.get(entity.name)) dan tidak dipanggil kode aplikasi — hanya test. Sisa ini dicatat di §5 dan todo 5.25.18 ⏸️.

Form. resolveForm() dengan konvensi penamaan lebih kaya dari yang didesain semula: form authored spesifik-mode ({entity}-create/-edit/-view) → form authored generik ({entity}-form) → derive. Blok Page/Tab yang menyebut Form lewat ref eksplisit selalu menang di atas keduanya, dan withEntityFieldDefaults() mengisi label/help yang tidak ditulis dari entity.

4. Route Table ​

buildRoutes() (shell/router.tsx) membangun daftar route sekali per surface dari bundle: route kind: Page dari spec.route, route CRUD turunan per entity (list/new/:id/:id/edit), plus satu route per entry Dashboard/Widget/Wizard/Kanban/Timeline/Report/Print (/dashboard/{name}, dst — mengikuti konvensi ../../spec/platform/02-workspace-app-module.md soal resolusi route view menu, menunggu Draft).

5. Status Implementasi Hari Ini ​

Lihat 01-architecture.md §5 untuk gap lintas-file (registry mati, menu derivation mati).

Ditutup 2026-10-08 (changelog 2026-10-08-008): klaim lama di sini — "kolom Table yang terpotong di atas 8 field hilang tanpa jejak" — sudah tidak benar sejak row-expand ada: deriveTable menaruh semua field di spec.columns, renderer menampilkan 8 pertama inline dan sisanya dibuka lewat toggle expand, dan itu memang kontraknya (../../spec/frontend/06-page-kinds.md §3.1). Yang benar-benar menyimpang sudah diperbaiki: tabel authored ikut terpotong 8 walau §3.1 menyatakan "columns eksplisit menang penuh". Kini tableColumnWindow(columns, authored) membedakan keduanya dari asal kolom (columnsAuthored pada memo resolusi spec), dengan penjaga di src/kinds/table/column-overflow.test.ts.

Masih terbuka:

  • resolveTable() (§3 di atas) adalah jalur override kedua yang predicate-nya berbeda dari yang dipakai TableRenderer (authoredTables.get(entity.name) vs t.spec.entity === "module.name") dan tidak dipakai kode aplikasi — hanya test. Dua implementasi presedensi yang sama = bahaya drift → todo 5.25.18 ⏸️.
  • N belum menyesuaikan lebar viewport; ia konstanta tetap 8 → todo 5.25.17 ⏸️.

Standar terbuka (CC0) dengan implementasi referensi.