prpm-dbp 0.13.2 → 0.14.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
@@ -13,6 +13,10 @@ Model boleh hasilkan Jawi yang nampak betul tetapi salah, dan pembaca tidak dapa
13
13
 
14
14
  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.
15
15
 
16
+ 0b. **Dua langkah untuk menentukan sesuatu perkataan betul.** Pertama, pastikan ia **ada dalam PRPM**. Kedua, semak **konteks ayat**. Langkah kedua tidak boleh dilangkau: ejaan Jawi menggunakan empat huruf vokal berbanding enam dalam Rumi, jadi `مڠيکوت` ialah ejaan sah bagi `mengikut` dan `mengekot` sekali gus. Kamus boleh mengesahkan perkataan itu wujud; hanya ayat boleh menentukan yang mana satu dimaksudkan.
17
+
18
+ 0c. **PRPM sendiri boleh tersilap.** Entri `menyebuk` membawa sebutan dan definisi yang betul tetapi ejaan Jawi `مڽبوت`, iaitu ejaan `menyebut`. Pakej menandakannya dalam medan `mencurigakan` apabila huruf akhir Jawi tidak sepadan dengan hujung Rumi. Tandaan itu bermakna semak sendiri di PRPM, bukan buang.
19
+
16
20
  1. **Jangan hantar Jawi yang belum disemak.** Setiap perkataan Jawi dalam output mesti datang dari PRPM atau ditanda belum disahkan.
17
21
  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.
18
22
  3. **Sebut sumber.** PRPM ada beberapa kamus. Bila petik definisi, sebut kamus mana (Kamus Dewan Edisi Keempat, Kamus Pelajar Edisi Kedua, dan lain-lain).
@@ -308,6 +312,21 @@ $ npx prpm-dbp jawi nota.txt --homograf
308
312
  homograf, ejaan Jawi dikongsi (3): اݢام=agama/igama, ساتو=satu/sato, بوروڠ=burung/borong
309
313
  ```
310
314
 
315
+ ## Laporan check-glosari
316
+
317
+ Empat kelas berasingan, kerana menggabungkannya menenggelamkan yang penting:
318
+
319
+ | Medan | Maksud |
320
+ |---|---|
321
+ | `bercanggah` | ejaan benar-benar berbeza daripada PRPM |
322
+ | `ejaanLama` | berbeza **hanya** pada pasangan ortografi lama/baharu yang boleh dipetakan: `ف`→`ڤ`, `ج`→`چ`, `ة`→`ه`, `ك`→`ک` |
323
+ | `homograf` | dua kata Rumi dalam glosari berkongsi ejaan Jawi yang sama |
324
+ | `mencurigakan` | glosari sepadan dengan PRPM, tetapi huruf akhir PRPM sendiri tidak munasabah |
325
+
326
+ `homograf` ialah satu-satunya kelas yang lolos senyap daripada semakan pasangan biasa: glosari boleh sepadan dengan PRPM dan **tetap salah** kerana ejaan itu dikongsi. `اية` sepadan dengan `ayah` dalam DBP, tetapi dalam teks al-Quran ia `آية` bermaksud `ayat`. Menerima DBP membuta di situ menghasilkan "menyatakan takrif **ayah** 1-5 Surah Al-Baqarah".
327
+
328
+ Penambahan huruf vokal **tidak** dikira ejaan lama. Kalau kehadiran `ي` diabaikan, `بينا` (bina) dan `بنا` (bena) akan kelihatan sama, sedangkan itu dua perkataan berlainan.
329
+
311
330
  ## Rantai kepercayaan
312
331
 
313
332
  Setiap ejaan Jawi yang dikeluarkan membawa asal-usulnya. Jangan runtuhkan lapisan ini.
package/bin/cli.js CHANGED
@@ -186,6 +186,12 @@ async function main() {
186
186
  h.dijanaPedoman.map((x) => `${x.kata}=${x.jawi} [${x.rujukan}]`).join(', '),
187
187
  )
188
188
  }
189
+ if (h.mencurigakan?.length) {
190
+ console.error(
191
+ `ejaan PRPM mencurigakan (${h.mencurigakan.length}): ` +
192
+ h.mencurigakan.map((x) => `${x.kata}=${x.jawi} (hujung ${x.hujungRumi} vs ${x.hujungJawi})`).join(', '),
193
+ )
194
+ }
189
195
  if (h.homograf?.length) {
190
196
  console.error(
191
197
  `homograf, ejaan Jawi dikongsi (${h.homograf.length}): ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prpm-dbp",
3
- "version": "0.13.2",
3
+ "version": "0.14.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
@@ -252,18 +252,52 @@ export async function auditJawi(jawiTeks, rumiTeks, cache, opts = {}) {
252
252
 
253
253
  /** Bandingkan glosari rumi->jawi sedia ada dengan ejaan rasmi PRPM. */
254
254
  export async function semakGlosari(glosari, cache, opts = {}) {
255
+ const { ejaanLamaSahaja, semakHurufAkhir } = await import('./semak.js')
255
256
  const bercanggah = []
257
+ const ejaanLama = []
258
+ const mencurigakan = []
256
259
  const tiadaDalamPrpm = []
257
260
  let sepadan = 0
258
261
  const senarai = Object.entries(glosari)
259
262
 
263
+ // Homograf ialah satu-satunya kelas ralat yang lolos senyap daripada
264
+ // semakan pasangan: glosari boleh sepadan dengan PRPM dan tetap salah
265
+ // kerana ejaan itu dikongsi perkataan lain. اية sepadan dengan `ayah`
266
+ // dalam DBP, tetapi dalam teks al-Quran ia آية bermaksud `ayat`.
267
+ const ikutJawi = new Map()
268
+ for (const [rumi, jawi] of senarai) {
269
+ if (!ikutJawi.has(jawi)) ikutJawi.set(jawi, [])
270
+ ikutJawi.get(jawi).push(rumi)
271
+ }
272
+
260
273
  for (const [i, [rumi, jawi]] of senarai.entries()) {
261
274
  const rec = await lookup(rumi, cache, opts)
262
275
  if (!rec.jawi) tiadaDalamPrpm.push(rumi)
263
- else if (rec.jawi === jawi || (rec.jawiVarian ?? []).includes(jawi)) sepadan++
264
- else bercanggah.push({ rumi, dalamGlosari: jawi, prpm: rec.jawi, url: rec.url })
276
+ else if (rec.jawi === jawi || (rec.jawiVarian ?? []).includes(jawi)) {
277
+ sepadan++
278
+ const curiga = semakHurufAkhir(rumi, rec.jawi)
279
+ if (curiga) mencurigakan.push(curiga)
280
+ } else if (ejaanLamaSahaja(jawi, rec.jawi)) {
281
+ // Ejaan lama boleh dipetakan kepada ejaan baharu. Bukan salah,
282
+ // cuma ortografi yang berbeza zaman.
283
+ ejaanLama.push({ rumi, dalamGlosari: jawi, prpm: rec.jawi, url: rec.url })
284
+ } else {
285
+ bercanggah.push({ rumi, dalamGlosari: jawi, prpm: rec.jawi, url: rec.url })
286
+ }
265
287
  opts.onProgress?.(i + 1, senarai.length)
266
288
  }
267
289
 
268
- return { jumlah: senarai.length, sepadan, bercanggah, tiadaDalamPrpm }
290
+ const homograf = [...ikutJawi]
291
+ .filter(([, kata]) => kata.length > 1)
292
+ .map(([jawi, kata]) => ({ jawi, dikongsi: kata }))
293
+
294
+ return {
295
+ jumlah: senarai.length,
296
+ sepadan,
297
+ bercanggah,
298
+ ejaanLama,
299
+ homograf,
300
+ mencurigakan,
301
+ tiadaDalamPrpm,
302
+ }
269
303
  }
package/src/semak.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Semakan kewarasan ejaan: adakah huruf akhir Jawi sepadan dengan hujung Rumi?
3
+ *
4
+ * Diperlukan kerana PRPM sendiri kadangkala menyimpan ejaan yang salah pada
5
+ * kata kepala. Contoh sebenar: entri `menyebuk` membawa sebutan [me.nye.buʔ]
6
+ * dan definisi "masuk mencelah" yang betul, tetapi ejaan Jawi مڽبوت, iaitu
7
+ * ejaan `menyebut`. Huruf akhir ت bermaksud "t", sedangkan menyebuk berakhir
8
+ * dengan "k". Ekstraksi kita setia; sumbernya yang rosak.
9
+ *
10
+ * Diukur terhadap 37,801 pasangan sebenar: 99.6% sepadan. Yang tidak sepadan
11
+ * hampir kesemuanya kata serapan Arab yang mengekalkan ortografi asal
12
+ * (bahas بحث, asah اصح), jadi huruf Arab disenaraikan sebagai padanan sah.
13
+ *
14
+ * Ini menandakan, bukan menolak. Ejaan yang mencurigakan tetap dipulangkan
15
+ * supaya pengguna melihatnya dan boleh menyemak sendiri di PRPM.
16
+ */
17
+
18
+ // Padanan huruf akhir Rumi kepada huruf akhir Jawi yang boleh diterima.
19
+ // Varian Arab disertakan kerana §11.1 mengekalkan ejaan asal bagi istilah
20
+ // khusus: bahas بحث guna ث untuk s, asah اصح guna ح untuk h.
21
+ const PADANAN_AKHIR = new Map([
22
+ ['t', ['ت', 'ة', 'ط']],
23
+ ['n', ['ن']],
24
+ ['m', ['م']],
25
+ ['h', ['ه', 'ة', 'ح']],
26
+ ['r', ['ر']],
27
+ ['l', ['ل']],
28
+ ['s', ['س', 'ص', 'ث']],
29
+ ['p', ['ڤ', 'ف']],
30
+ ['g', ['ݢ', 'ڬ']],
31
+ ['k', ['ق', 'ک', 'ك', 'ء']],
32
+ ['d', ['د', 'ض']],
33
+ ['b', ['ب']],
34
+ ['f', ['ف']],
35
+ ['z', ['ز', 'ظ', 'ذ']],
36
+ ])
37
+
38
+ /**
39
+ * Pulangkan null jika waras atau tidak dapat disemak; pulangkan butiran
40
+ * jika huruf akhir tidak sepadan.
41
+ */
42
+ export function semakHurufAkhir(rumi, jawi) {
43
+ if (!rumi || !jawi) return null
44
+ const kata = String(rumi).trim().toLowerCase()
45
+ const ejaan = String(jawi).trim()
46
+ if (kata.includes('-') || kata.length < 3) return null
47
+
48
+ // -ng dan -ny ialah digraf; huruf akhirnya bukan penunjuk yang sah.
49
+ if (kata.endsWith('ng') || kata.endsWith('ny')) return null
50
+
51
+ const jangkaan = PADANAN_AKHIR.get(kata.slice(-1))
52
+ if (!jangkaan) return null
53
+
54
+ const akhirJawi = ejaan.slice(-1)
55
+ if (jangkaan.includes(akhirJawi)) return null
56
+
57
+ return {
58
+ kata,
59
+ jawi: ejaan,
60
+ hujungRumi: kata.slice(-1),
61
+ hujungJawi: akhirJawi,
62
+ dijangka: jangkaan,
63
+ sebab: 'huruf akhir Jawi tidak sepadan dengan hujung Rumi',
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Bezakan ejaan Jawi LAMA daripada ejaan yang benar-benar salah.
69
+ *
70
+ * Dokumen kurikulum kerap menggunakan ortografi lama. Perbezaannya boleh
71
+ * dipetakan dan tidak bermakna ejaannya salah:
72
+ * ف -> ڤ (fa Arab digunakan untuk bunyi p sebelum ڤ digubal)
73
+ * ج -> چ (jim digunakan untuk bunyi c sebelum چ digubal)
74
+ * ة -> ه (ta marbutah ditukar ha pada sesetengah kata terserap)
75
+ * ك -> ک (kaf Arab lawan kaf Jawi)
76
+ * ي -> ى (ya lawan alif maqsurah)
77
+ *
78
+ * Melaporkannya sebagai "bercanggah" bersama ejaan yang betul-betul salah
79
+ * menenggelamkan yang penting. Ia kelas berasingan.
80
+ */
81
+ const PASANGAN_LAMA = [
82
+ ['ف', 'ڤ'],
83
+ ['ج', 'چ'],
84
+ ['ة', 'ه'],
85
+ ['ك', 'ک'],
86
+ ['ي', 'ى'],
87
+ ['ݢ', 'ڬ'],
88
+ ]
89
+
90
+ function samakanLama(teks) {
91
+ let keluar = teks
92
+ for (const [lama, baharu] of PASANGAN_LAMA) {
93
+ keluar = keluar.split(lama).join(baharu)
94
+ }
95
+ return keluar
96
+ }
97
+
98
+ /**
99
+ * Adakah dua ejaan berbeza HANYA pada pasangan lama/baharu yang diketahui?
100
+ */
101
+ export function ejaanLamaSahaja(a, b) {
102
+ if (!a || !b || a === b) return false
103
+ return samakanLama(a) === samakanLama(b)
104
+ }
package/src/tukar.js CHANGED
@@ -12,6 +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
16
  import {
16
17
  imbuhAkhiranAn,
17
18
  imbuhAkhiranKan,
@@ -256,6 +257,7 @@ export async function keJawi(teks, cache, opts = {}) {
256
257
  const kataUlang = []
257
258
  const dijanaPedoman = []
258
259
  const akronim = []
260
+ const mencurigakan = []
259
261
  const bahagian = teks.split(PECAH_RUMI)
260
262
  const keluar = []
261
263
 
@@ -263,7 +265,15 @@ export async function keJawi(teks, cache, opts = {}) {
263
265
  if (i % 2 === 0) { keluar.push(cebis); continue }
264
266
 
265
267
  const rec = await lookup(cebis.toLowerCase(), cache, opts)
266
- if (rec?.jawi) { keluar.push(rec.jawi); dbp.push(cebis); continue }
268
+ if (rec?.jawi) {
269
+ // PRPM sendiri kadangkala menyimpan ejaan salah pada kata kepala.
270
+ // Ditandakan, bukan ditolak: pengguna perlu melihatnya.
271
+ const curiga = semakHurufAkhir(cebis, rec.jawi)
272
+ if (curiga) mencurigakan.push(curiga)
273
+ keluar.push(rec.jawi)
274
+ dbp.push(cebis)
275
+ continue
276
+ }
267
277
 
268
278
  // Keadaan berbeza daripada tiada entri: DBP mengiktiraf perkataan ini
269
279
  // sebagai Melayu, cuma tidak menyimpan ejaan Jawinya. Lazim bagi nama
@@ -387,6 +397,7 @@ export async function keJawi(teks, cache, opts = {}) {
387
397
  dijanaPedoman,
388
398
  namaKhas: [...new Set(namaKhas)],
389
399
  akronim: [...new Set(akronim)],
400
+ mencurigakan,
390
401
  adaEntriTanpaJawi: [...new Set(adaEntriTanpaJawi)],
391
402
  belumDisahkan: [...new Set(belumDisahkan)],
392
403
  terbitanSah,