prpm-dbp 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -9,6 +9,30 @@ Sumber kebenaran tunggal untuk ejaan BM dan Jawi ialah PRPM DBP, bukan ingatan m
9
9
 
10
10
  Model boleh hasilkan Jawi yang nampak betul tetapi salah, dan pembaca tidak dapat kesan. Skill ini tukar tekaan jadi semakan.
11
11
 
12
+ ## Prosedur wajib
13
+
14
+ Ikut ini setiap kali, jangan langkau langkah.
15
+
16
+ **Menulis Jawi daripada Rumi:**
17
+
18
+ 1. `npx prpm-dbp jawi <fail> --luar-talian --homograf --json`
19
+ 2. Baca `belumDisahkan` dan `namaKhas`. Jangan reka ejaan untuknya. Sahkan dengan `lookup`, atau tanya pengguna, atau tandakan dalam hasil akhir.
20
+ 3. Baca `mencurigakan`. Setiap satu bermakna PRPM sendiri mungkin tersilap. Semak di pautan yang diberi sebelum menggunakannya.
21
+ 4. Baca `homograf`. Beritahu pengguna perkataan mana yang akan taksa apabila dibaca.
22
+ 5. Laporkan `semuanyaDbp`. Jika `false`, nyatakan bahagian mana bukan daripada sumber rasmi.
23
+
24
+ **Merumikan daripada Jawi:**
25
+
26
+ 1. `npx prpm-dbp rumi <fail> --pilih --konteks --json`
27
+ 2. Untuk **setiap** item dalam `kabur`: baca ayat yang mengandunginya, baca makna setiap calon, tentukan yang mana sesuai.
28
+ 3. Jika `dipilih` salah bagi ayat itu, **gantikan** dalam hasil akhir. Kekerapan kalah kepada konteks.
29
+ 4. Nyatakan kepada pengguna berapa banyak kekaburan wujud dan mana yang anda ubah.
30
+ 5. `tidakDikenali` bermakna perkataan itu tiada dalam cache. Jalankan `warm` di luar talian, bukan tinggalkan bertanda.
31
+
32
+ **Berhenti dan tanya pengguna apabila:** ejaan `mencurigakan` mengubah makna, nama khas tiada rujukan bertulis, atau dua calon sama-sama munasabah dalam ayat itu.
33
+
34
+ Semua perintah menyokong `--json`, jadi langkah di atas boleh dibuat secara berulang tanpa menghurai teks.
35
+
12
36
  ## Peraturan
13
37
 
14
38
  0. **Semak ejaan ikut DBP, bukan ikut ingatan.** Ini peraturan pertama kerana ia yang paling kerap dilanggar. Perkataan yang anda "tahu" ejaannya masih perlu disemak. Contoh sebenar daripada penggunaan: `sunah` ialah `سنة` tetapi `sunat` ialah `سونت`; `bena` langsung tiada ejaan Jawi dalam PRPM walaupun ia perkataan yang sah; `salat` hanyalah rujukan silang kepada `solat`. Tiada satu pun daripada perbezaan ini boleh diteka daripada bentuk Rumi. Jalankan `lookup` atau `jawi`, jangan tulis daripada ingatan.
@@ -30,6 +54,23 @@ Model boleh hasilkan Jawi yang nampak betul tetapi salah, dan pembaca tidak dapa
30
54
 
31
55
  Bilangan rujukan itu datang daripada panel korpus DBP sendiri, bukan anggaran. Ia **mengisih** calon supaya yang lazim dilihat dahulu; ia **tidak memilih**. Perkataan jarang tetap betul apabila ayat menghendakinya.
32
56
 
57
+ 0e. **Pembahagian kerja: alat mengemukakan, ANDA memutuskan.** CLI tidak boleh membaca konteks; anda boleh. Jangan sesekali menghantar `mengikut|mengekot` sebagai hasil akhir kepada pengguna.
58
+
59
+ Aliran yang betul:
60
+
61
+ ```bash
62
+ npx prpm-dbp rumi teks.txt --pilih --konteks
63
+ ```
64
+
65
+ `--pilih` menghasilkan teks bersih dengan memilih calon paling lazim, dan menyenaraikan setiap pilihan itu:
66
+
67
+ ```
68
+ itu mengikut sukatan pelajaran.
69
+ dipilih ikut kekerapan (SEMAK konteks): مڠيکوت -> mengikut [lain: mengekot]
70
+ ```
71
+
72
+ Setiap baris `dipilih` ialah tekaan berasaskan kekerapan, **bukan pemahaman konteks**. Baca ayatnya. Dalam contoh di atas `mengikut sukatan pelajaran` jelas betul dan `mengekot` mustahil. Tetapi dalam ayat tentang orang kesejukan merapatkan tangan ke badan, `mengekot` yang betul walaupun kekerapannya 6 berbanding 2439. Kekerapan ialah tekaan terbaik tanpa konteks; ia kalah kepada konteks setiap kali.
73
+
33
74
  1. **Jangan hantar Jawi yang belum disemak.** Setiap perkataan Jawi dalam output mesti datang dari PRPM atau ditanda belum disahkan.
34
75
  2. **Tanda, jangan sekat.** Kalau satu perkataan tiada dalam PRPM, hantar kerja itu dan senaraikan perkataan berkenaan di hujung. Jangan tahan seluruh dokumen sebab satu nama khas.
35
76
  3. **Sebut sumber.** PRPM ada beberapa kamus. Bila petik definisi, sebut kamus mana (Kamus Dewan Edisi Keempat, Kamus Pelajar Edisi Kedua, dan lain-lain).
@@ -143,6 +184,30 @@ Ejaan yang disimpan begini ditandakan datang daripada pengguna, bukan daripada D
143
184
 
144
185
  Bagi nama berasal Arab, ejaan Jawinya memang ejaan Arab asal, jadi ia nampak seperti dikekalkan. Bagi nama bukan Arab ia ditransliterasi penuh: `Kuala Lumpur` menjadi `کوالا لومڤور`.
145
186
 
187
+ ## Prosedur wajib
188
+
189
+ Ikut ini setiap kali, jangan langkau langkah.
190
+
191
+ **Menulis Jawi daripada Rumi:**
192
+
193
+ 1. `npx prpm-dbp jawi <fail> --luar-talian --homograf --json`
194
+ 2. Baca `belumDisahkan` dan `namaKhas`. Jangan reka ejaan untuknya. Sahkan dengan `lookup`, atau tanya pengguna, atau tandakan dalam hasil akhir.
195
+ 3. Baca `mencurigakan`. Setiap satu bermakna PRPM sendiri mungkin tersilap. Semak di pautan yang diberi sebelum menggunakannya.
196
+ 4. Baca `homograf`. Beritahu pengguna perkataan mana yang akan taksa apabila dibaca.
197
+ 5. Laporkan `semuanyaDbp`. Jika `false`, nyatakan bahagian mana bukan daripada sumber rasmi.
198
+
199
+ **Merumikan daripada Jawi:**
200
+
201
+ 1. `npx prpm-dbp rumi <fail> --pilih --konteks --json`
202
+ 2. Untuk **setiap** item dalam `kabur`: baca ayat yang mengandunginya, baca makna setiap calon, tentukan yang mana sesuai.
203
+ 3. Jika `dipilih` salah bagi ayat itu, **gantikan** dalam hasil akhir. Kekerapan kalah kepada konteks.
204
+ 4. Nyatakan kepada pengguna berapa banyak kekaburan wujud dan mana yang anda ubah.
205
+ 5. `tidakDikenali` bermakna perkataan itu tiada dalam cache. Jalankan `warm` di luar talian, bukan tinggalkan bertanda.
206
+
207
+ **Berhenti dan tanya pengguna apabila:** ejaan `mencurigakan` mengubah makna, nama khas tiada rujukan bertulis, atau dua calon sama-sama munasabah dalam ayat itu.
208
+
209
+ Semua perintah menyokong `--json`, jadi langkah di atas boleh dibuat secara berulang tanpa menghurai teks.
210
+
146
211
  ## Peraturan Pedoman DBP
147
212
 
148
213
  Satu fail sahaja dalam pakej ini **menjana** ejaan dan bukan mencarinya: `src/pedoman.js`. Ia dibenarkan kerana setiap peraturan di dalamnya datang daripada *Pedoman Umum Ejaan Jawi Bahasa Melayu* terbitan DBP, dan dirujuk pada seksyennya.
@@ -254,6 +319,30 @@ Dua puluh dua kes kabur yang tinggal itu sah: `قلقله` boleh dibaca `qalqala
254
319
 
255
320
  **Sifar perkataan hilang atau rosak.**
256
321
 
322
+ ## Prosedur wajib
323
+
324
+ Ikut ini setiap kali, jangan langkau langkah.
325
+
326
+ **Menulis Jawi daripada Rumi:**
327
+
328
+ 1. `npx prpm-dbp jawi <fail> --luar-talian --homograf --json`
329
+ 2. Baca `belumDisahkan` dan `namaKhas`. Jangan reka ejaan untuknya. Sahkan dengan `lookup`, atau tanya pengguna, atau tandakan dalam hasil akhir.
330
+ 3. Baca `mencurigakan`. Setiap satu bermakna PRPM sendiri mungkin tersilap. Semak di pautan yang diberi sebelum menggunakannya.
331
+ 4. Baca `homograf`. Beritahu pengguna perkataan mana yang akan taksa apabila dibaca.
332
+ 5. Laporkan `semuanyaDbp`. Jika `false`, nyatakan bahagian mana bukan daripada sumber rasmi.
333
+
334
+ **Merumikan daripada Jawi:**
335
+
336
+ 1. `npx prpm-dbp rumi <fail> --pilih --konteks --json`
337
+ 2. Untuk **setiap** item dalam `kabur`: baca ayat yang mengandunginya, baca makna setiap calon, tentukan yang mana sesuai.
338
+ 3. Jika `dipilih` salah bagi ayat itu, **gantikan** dalam hasil akhir. Kekerapan kalah kepada konteks.
339
+ 4. Nyatakan kepada pengguna berapa banyak kekaburan wujud dan mana yang anda ubah.
340
+ 5. `tidakDikenali` bermakna perkataan itu tiada dalam cache. Jalankan `warm` di luar talian, bukan tinggalkan bertanda.
341
+
342
+ **Berhenti dan tanya pengguna apabila:** ejaan `mencurigakan` mengubah makna, nama khas tiada rujukan bertulis, atau dua calon sama-sama munasabah dalam ayat itu.
343
+
344
+ Semua perintah menyokong `--json`, jadi langkah di atas boleh dibuat secara berulang tanpa menghurai teks.
345
+
257
346
  ## Peraturan yang sengaja TIDAK dilaksanakan
258
347
 
259
348
  | Seksyen | Sebab |
@@ -340,6 +429,34 @@ Empat kelas berasingan, kerana menggabungkannya menenggelamkan yang penting:
340
429
 
341
430
  Penambahan huruf vokal **tidak** dikira ejaan lama. Kalau kehadiran `ي` diabaikan, `بينا` (bina) dan `بنا` (bena) akan kelihatan sama, sedangkan itu dua perkataan berlainan.
342
431
 
432
+ ## Ejaan bukan piawai DBP
433
+
434
+ Dokumen kurikulum Malaysia kerap menggunakan ortografi Jawi yang berbeza daripada piawai DBP. Pasangan yang disahkan daripada DSKP Pendidikan Islam KPM sebenar:
435
+
436
+ | Rumi | DBP | DSKP | Beza |
437
+ |---|---|---|---|
438
+ | surah | `سورة` | `سوره` | ta marbutah jadi ha |
439
+ | iktibar | `اعتبار` | `اعتبر` | alif digugurkan |
440
+ | bersifat | `برصيفت` | `برصفة` | ya digugurkan, ta marbutah |
441
+ | dalil | `دليل` | `داليل` | alif ditambah |
442
+ | syirik | `شيريک` | `شرك` | ya digugurkan, kaf Arab |
443
+
444
+ Kesemuanya berbeza hanya pada huruf vokal dan bentuk huruf, bukan pada rangka konsonan. `--rangka` memadan rangka itu:
445
+
446
+ ```
447
+ $ npx prpm-dbp rumi dskp.txt --luar-talian --rangka
448
+ ejaan bukan piawai DBP (209):
449
+ اعتبر -> iktibar [DBP eja: اعتبار]
450
+ سوره -> surah [DBP eja: سورة]
451
+ کتب -> kitab|katib|kutub (rangka sama, PILIH ikut konteks)
452
+ ```
453
+
454
+ Diukur pada 168 baris DSKP sebenar: token yang langsung tidak dikenali turun daripada 100 kepada 47.
455
+
456
+ **Ini lapisan terakhir dan ia LOSSY.** 29% rangka dikongsi lebih daripada satu perkataan, purata 4.1 calon. Ia hanya digunakan selepas padanan tepat gagal, hasilnya sentiasa berlabel `ejaanBukanPiawai` berserta ejaan DBP untuk perbandingan, dan ia **tidak pernah memilih sendiri** apabila calon berbilang.
457
+
458
+ Kegunaannya bukan menjadikan alat lebih longgar. Ia membezakan **"ini surah, cuma dieja lain"** daripada **"saya tidak kenal benda ini"** — dua keadaan yang dahulu keluar sama sebagai `«...»`.
459
+
343
460
  ## Rantai kepercayaan
344
461
 
345
462
  Setiap ejaan Jawi yang dikeluarkan membawa asal-usulnya. Jangan runtuhkan lapisan ini.
package/bin/cli.js CHANGED
@@ -38,6 +38,9 @@ Pilihan: --refresh --had <n> --delay <ms> --json --cepat
38
38
  --luar-talian jangan sentuh rangkaian langsung (wajib untuk laluan permintaan app)
39
39
  --homograf tandakan perkataan yang ejaan Jawinya dikongsi perkataan lain
40
40
  --konteks papar ayat dan makna setiap calon bagi kekaburan (rumi)
41
+ --rangka padan ejaan bukan piawai melalui rangka konsonan (berlabel)
42
+ --pilih pilih calon paling lazim supaya teks boleh terus dibaca;
43
+ setiap pilihan disenaraikan untuk disemak ikut konteks
41
44
 
42
45
  Cache: ${CACHE_PATH}
43
46
  `
@@ -55,6 +58,8 @@ function huraiArgs(argv) {
55
58
  else if (a === '--luar-talian') pilihan.luarTalian = true
56
59
  else if (a === '--homograf') pilihan.homograf = true
57
60
  else if (a === '--konteks') pilihan.konteks = true
61
+ else if (a === '--pilih') pilihan.pilih = true
62
+ else if (a === '--rangka') pilihan.rangka = true
58
63
  else if (a === '--jenis') pilihan.jenis = argv[++i]
59
64
  else if (a === '--had') pilihan.had = Number(argv[++i])
60
65
  else if (a === '--delay') pilihan.delay = Number(argv[++i])
@@ -220,7 +225,11 @@ async function main() {
220
225
 
221
226
  case 'rumi': {
222
227
  const teks = await readFile(baki[0], 'utf8')
223
- const h = keRumi(teks, cache, { dataset: await muatDataset() })
228
+ const h = keRumi(teks, cache, {
229
+ dataset: await muatDataset(),
230
+ pilihLazim: pilihan.pilih,
231
+ padanRangka: pilihan.rangka,
232
+ })
224
233
  if (pilihan.json) { keluar(h); break }
225
234
  console.log(h.teks)
226
235
  console.error(`\npeta songsang: ${h.saizPeta} ejaan`)
@@ -249,8 +258,25 @@ async function main() {
249
258
  }
250
259
  }
251
260
  }
261
+ if (h.ejaanBukanPiawai?.length) {
262
+ const tunggal = h.ejaanBukanPiawai.filter((x) => x.calon.length === 1)
263
+ const banyak = h.ejaanBukanPiawai.filter((x) => x.calon.length > 1)
264
+ console.error(`\nejaan bukan piawai DBP (${h.ejaanBukanPiawai.length}):`)
265
+ for (const x of tunggal.slice(0, 20)) {
266
+ console.error(` ${x.jawi} -> ${x.calon[0].rumi} [DBP eja: ${x.calon[0].jawiDbp}]`)
267
+ }
268
+ for (const x of banyak.slice(0, 10)) {
269
+ console.error(` ${x.jawi} -> ${x.calon.map((c) => c.rumi).join('|')} (rangka sama, PILIH ikut konteks)`)
270
+ }
271
+ }
252
272
  if (h.arab.length) console.error(`teks Arab dibiarkan (${h.arab.length}): ${h.arab.join(' ')}`)
253
- for (const k of h.kabur) console.error(`kabur: ${k.jawi} -> ${k.calon.join(', ')}`)
273
+ for (const k of h.kabur) {
274
+ console.error(
275
+ k.dipilih
276
+ ? `dipilih ikut kekerapan (SEMAK konteks): ${k.jawi} -> ${k.dipilih} [lain: ${k.calon.slice(1).join(', ')}]`
277
+ : `kabur: ${k.jawi} -> ${k.calon.join(', ')}`,
278
+ )
279
+ }
254
280
  break
255
281
  }
256
282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prpm-dbp",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Semak ejaan Bahasa Melayu dan Jawi terhadap PRPM Dewan Bahasa dan Pustaka. CLI dan skill untuk AI agent.",
5
5
  "keywords": [
6
6
  "bahasa-melayu",
package/src/prpm.js CHANGED
@@ -127,10 +127,43 @@ export async function loadCache() {
127
127
  }
128
128
  }
129
129
 
130
+ /**
131
+ * Simpan cache dengan MENGGABUNGKAN keadaan di cakera terlebih dahulu.
132
+ *
133
+ * Menulis ganti secara membuta memusnahkan kerja proses lain. Dua sesi yang
134
+ * berjalan serentak akan memuat keadaan yang sama, masing-masing menambah
135
+ * perkataannya, dan yang menyimpan kemudian memadam tambahan yang awal.
136
+ * Ia diperhatikan secara langsung: entri yang baru dipanaskan hilang selepas
137
+ * sesi lain menyimpan.
138
+ *
139
+ * Rekod di cakera menang bagi kunci yang sama, kerana ia mungkin lebih baharu.
140
+ * Rekod benih (`ringkas`) tidak pernah menewaskan rekod penuh.
141
+ */
130
142
  export async function saveCache(cache) {
131
143
  await mkdir(dirname(CACHE_PATH), { recursive: true })
144
+
145
+ let diCakera = {}
146
+ try {
147
+ diCakera = JSON.parse(await readFile(CACHE_PATH, 'utf8'))
148
+ } catch {
149
+ diCakera = {}
150
+ }
151
+
152
+ const gabung = { ...cache }
153
+ for (const [kata, rec] of Object.entries(diCakera)) {
154
+ const sedia = gabung[kata]
155
+ // Rekod penuh mengatasi rekod ringkas; selain itu cakera menang.
156
+ if (sedia && !sedia.ringkas && rec.ringkas) continue
157
+ gabung[kata] = rec
158
+ }
159
+
160
+ // Rekod benih tidak ditulis ke cache pengguna: ia sudah ada dalam pakej.
161
+ for (const [kata, rec] of Object.entries(gabung)) {
162
+ if (rec?.benih) delete gabung[kata]
163
+ }
164
+
132
165
  const tmp = `${CACHE_PATH}.tmp`
133
- await writeFile(tmp, JSON.stringify(cache, null, 1), 'utf8')
166
+ await writeFile(tmp, JSON.stringify(gabung, null, 1), 'utf8')
134
167
  await rename(tmp, CACHE_PATH)
135
168
  }
136
169
 
package/src/semak.js CHANGED
@@ -102,3 +102,53 @@ export function ejaanLamaSahaja(a, b) {
102
102
  if (!a || !b || a === b) return false
103
103
  return samakanLama(a) === samakanLama(b)
104
104
  }
105
+
106
+ /**
107
+ * Rangka konsonan: buang huruf vokal pilihan dan samakan varian bentuk.
108
+ *
109
+ * Dokumen kurikulum Malaysia kerap menggunakan ejaan Jawi yang berbeza
110
+ * daripada piawai DBP. Corak yang disahkan pada pasangan sebenar DSKP:
111
+ *
112
+ * surah DBP سورة DSKP سوره ta marbutah jadi ha
113
+ * iktibar DBP اعتبار DSKP اعتبر alif digugurkan
114
+ * bersifat DBP برصيفت DSKP برصفة ya digugurkan, ta marbutah
115
+ * dalil DBP دليل DSKP داليل alif ditambah
116
+ * syirik DBP شيريک DSKP شرك ya digugurkan, kaf Arab
117
+ *
118
+ * Kesemuanya berbeza hanya pada huruf vokal dan bentuk huruf, bukan pada
119
+ * rangka konsonan. Membuang vokal menjadikan kedua-dua ejaan sepadan.
120
+ *
121
+ * INI LOSSY dan MESTI kekal di bawah lapisan 1 dan 2. Banyak perkataan
122
+ * berkongsi rangka yang sama, jadi padanan begini adalah inferens, bukan
123
+ * rekod DBP, dan hasilnya mesti berlabel.
124
+ */
125
+ const VOKAL_PILIHAN = /[اويى]/g
126
+
127
+ const SAMA_BENTUK = new Map([
128
+ ['ة', 'ه'],
129
+ ['ك', 'ک'],
130
+ ['ڬ', 'ݢ'],
131
+ ['ف', 'ڤ'],
132
+ ['ج', 'چ'],
133
+ ['أ', 'ا'],
134
+ ['إ', 'ا'],
135
+ ['ء', ''],
136
+ ])
137
+
138
+ // Pedoman §6.4: ta marbutah melambangkan [t] ATAU [h] sebagai kelainan
139
+ // sebutan pada kata serapan Arab (akibat/akibah, hidayat/hidayah). Ketiga-tiga
140
+ // huruf itu boleh bertukar di HUJUNG kata sahaja. Di tengah kata ia huruf
141
+ // berlainan dan tidak boleh dilipat.
142
+ const HUJUNG_SETARA = new Set(['ة', 'ه', 'ت'])
143
+
144
+ export function rangkaKonsonan(jawi) {
145
+ if (!jawi) return ''
146
+ let keluar = ''
147
+ for (const huruf of String(jawi).normalize('NFC')) {
148
+ const ganti = SAMA_BENTUK.has(huruf) ? SAMA_BENTUK.get(huruf) : huruf
149
+ keluar += ganti
150
+ }
151
+ keluar = keluar.replace(VOKAL_PILIHAN, '').replace(/\s+/g, '')
152
+ if (keluar && HUJUNG_SETARA.has(keluar.slice(-1))) keluar = keluar.slice(0, -1) + 'ه'
153
+ return keluar
154
+ }
package/src/tukar.js CHANGED
@@ -12,7 +12,7 @@ import { lookup } from './prpm.js'
12
12
  import { normalisasiJawi } from './kamus.js'
13
13
  import { cariDataset, muatDataset } from './dataset.js'
14
14
  import { cariNama, mungkinAkronim, mungkinNamaKhas } from './nama.js'
15
- import { semakHurufAkhir } from './semak.js'
15
+ import { rangkaKonsonan, semakHurufAkhir } from './semak.js'
16
16
  import {
17
17
  imbuhAkhiranAn,
18
18
  imbuhAkhiranKan,
@@ -162,6 +162,33 @@ export async function cariDasar(kata, cache, opts = {}) {
162
162
  * bilangan perkataan terpanjang supaya pembacaan songsang boleh memadan
163
163
  * jujukan, bukan token tunggal sahaja.
164
164
  */
165
+ /**
166
+ * Indeks rangka konsonan, untuk memadan ejaan bukan piawai.
167
+ *
168
+ * Dokumen kurikulum Malaysia kerap menggunakan ortografi yang berbeza
169
+ * daripada DBP: vokal digugur atau ditambah, ta marbutah bertukar ha, bentuk
170
+ * Arab menggantikan bentuk Jawi Melayu. Indeks ini memadan rangka konsonan
171
+ * supaya `سوره` masih dikenali sebagai `surah`.
172
+ *
173
+ * LOSSY: 29% rangka dikongsi lebih daripada satu perkataan, purata 4.1 calon.
174
+ * Kerana itu ia hanya digunakan selepas padanan tepat gagal, hasilnya sentiasa
175
+ * berlabel, dan ia TIDAK PERNAH memilih sendiri apabila calon berbilang.
176
+ */
177
+ export function petaRangka(cache, dataset = null) {
178
+ const peta = new Map()
179
+ const tambah = (jawi, rumi) => {
180
+ const kunci = rangkaKonsonan(jawi)
181
+ if (!kunci) return
182
+ if (!peta.has(kunci)) peta.set(kunci, new Map())
183
+ if (!peta.get(kunci).has(rumi)) peta.get(kunci).set(rumi, jawi)
184
+ }
185
+ for (const rec of Object.values(cache)) if (rec?.jawi) tambah(rec.jawi, rec.kata)
186
+ for (const [rumi, ejaan] of Object.entries(dataset?.peta ?? {})) {
187
+ for (const j of ejaan) tambah(j, rumi)
188
+ }
189
+ return peta
190
+ }
191
+
165
192
  export function petaSongsang(cache, dataset = null) {
166
193
  const peta = new Map()
167
194
  let maxKata = 1
@@ -425,12 +452,18 @@ export async function keJawi(teks, cache, opts = {}) {
425
452
  * menggugurkannya. Kerana itu perlanggaran jauh lebih jarang daripada yang
426
453
  * disangka. Bila ia berlaku, SEMUA calon dilaporkan, bukan dipilih satu.
427
454
  */
428
- export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null } = {}) {
455
+ export function keRumi(
456
+ teksJawi,
457
+ cache,
458
+ { penanda = ['«', '»'], dataset = null, pilihLazim = false, padanRangka = false } = {},
459
+ ) {
429
460
  const peta = petaSongsang(cache, dataset)
430
461
  const tidakDikenali = []
431
462
  const kabur = []
432
463
  const arab = []
433
464
  const sendiDipisah = []
465
+ const bukanPiawai = []
466
+ const rangka = padanRangka ? petaRangka(cache, dataset) : null
434
467
  const bahagian = teksJawi.split(PECAH_JAWI)
435
468
  const keluar = []
436
469
 
@@ -487,6 +520,22 @@ export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null
487
520
  }
488
521
  if (dipisah) { keluar.push(dipisah); sendiDipisah.push(bahagian[i]); continue }
489
522
 
523
+ // Lapisan terakhir: ejaan bukan piawai. Berlabel, di bawah lapisan
524
+ // 1 dan 2, dan tidak pernah memilih sendiri apabila calon berbilang.
525
+ if (rangka) {
526
+ const padananRangka = rangka.get(rangkaKonsonan(bahagian[i]))
527
+ if (padananRangka?.size) {
528
+ const senarai = [...padananRangka.entries()].map(([rumi, jawiDbp]) => ({ rumi, jawiDbp }))
529
+ bukanPiawai.push({ jawi: bahagian[i], calon: senarai })
530
+ if (senarai.length === 1) {
531
+ keluar.push(senarai[0].rumi)
532
+ continue
533
+ }
534
+ keluar.push(penanda[0] + senarai.map((x) => x.rumi).join('|') + penanda[1])
535
+ continue
536
+ }
537
+ }
538
+
490
539
  tidakDikenali.push(bahagian[i])
491
540
  keluar.push(penanda[0] + bahagian[i] + penanda[1])
492
541
  continue
@@ -494,8 +543,19 @@ export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null
494
543
 
495
544
  if (padanan.length === 1) keluar.push(padanan[0])
496
545
  else {
497
- kabur.push({ jawi: bahagian.slice(i, hujung + 1).join(''), calon: padanan })
498
- keluar.push(padanan.join('|'))
546
+ const rekod = { jawi: bahagian.slice(i, hujung + 1).join(''), calon: padanan }
547
+ kabur.push(rekod)
548
+ // pilihLazim menghasilkan teks yang boleh terus dibaca dengan memilih
549
+ // calon paling lazim dalam korpus DBP. Ia BUKAN pemahaman konteks:
550
+ // setiap pilihan direkod dalam medan `kabur` supaya agen atau manusia
551
+ // yang membaca ayat boleh menggantikannya. Kekerapan ialah tekaan
552
+ // terbaik tanpa konteks, bukan jawapan.
553
+ if (pilihLazim) {
554
+ rekod.dipilih = padanan[0]
555
+ keluar.push(padanan[0])
556
+ } else {
557
+ keluar.push(padanan.join('|'))
558
+ }
499
559
  }
500
560
  i = hujung
501
561
  }
@@ -506,6 +566,7 @@ export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null
506
566
  kabur,
507
567
  arab: [...new Set(arab)],
508
568
  sendiDipisah,
569
+ ejaanBukanPiawai: bukanPiawai,
509
570
  saizPeta: peta.size,
510
571
  lengkap: tidakDikenali.length === 0,
511
572
  }