prpm-dbp 0.16.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.
@@ -160,6 +184,30 @@ Ejaan yang disimpan begini ditandakan datang daripada pengguna, bukan daripada D
160
184
 
161
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 `کوالا لومڤور`.
162
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
+
163
211
  ## Peraturan Pedoman DBP
164
212
 
165
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.
@@ -271,6 +319,30 @@ Dua puluh dua kes kabur yang tinggal itu sah: `قلقله` boleh dibaca `qalqala
271
319
 
272
320
  **Sifar perkataan hilang atau rosak.**
273
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
+
274
346
  ## Peraturan yang sengaja TIDAK dilaksanakan
275
347
 
276
348
  | Seksyen | Sebab |
@@ -357,6 +429,34 @@ Empat kelas berasingan, kerana menggabungkannya menenggelamkan yang penting:
357
429
 
358
430
  Penambahan huruf vokal **tidak** dikira ejaan lama. Kalau kehadiran `ي` diabaikan, `بينا` (bina) dan `بنا` (bena) akan kelihatan sama, sedangkan itu dua perkataan berlainan.
359
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
+
360
460
  ## Rantai kepercayaan
361
461
 
362
462
  Setiap ejaan Jawi yang dikeluarkan membawa asal-usulnya. Jangan runtuhkan lapisan ini.
package/bin/cli.js CHANGED
@@ -38,6 +38,7 @@ 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)
41
42
  --pilih pilih calon paling lazim supaya teks boleh terus dibaca;
42
43
  setiap pilihan disenaraikan untuk disemak ikut konteks
43
44
 
@@ -58,6 +59,7 @@ function huraiArgs(argv) {
58
59
  else if (a === '--homograf') pilihan.homograf = true
59
60
  else if (a === '--konteks') pilihan.konteks = true
60
61
  else if (a === '--pilih') pilihan.pilih = true
62
+ else if (a === '--rangka') pilihan.rangka = true
61
63
  else if (a === '--jenis') pilihan.jenis = argv[++i]
62
64
  else if (a === '--had') pilihan.had = Number(argv[++i])
63
65
  else if (a === '--delay') pilihan.delay = Number(argv[++i])
@@ -226,6 +228,7 @@ async function main() {
226
228
  const h = keRumi(teks, cache, {
227
229
  dataset: await muatDataset(),
228
230
  pilihLazim: pilihan.pilih,
231
+ padanRangka: pilihan.rangka,
229
232
  })
230
233
  if (pilihan.json) { keluar(h); break }
231
234
  console.log(h.teks)
@@ -255,6 +258,17 @@ async function main() {
255
258
  }
256
259
  }
257
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
+ }
258
272
  if (h.arab.length) console.error(`teks Arab dibiarkan (${h.arab.length}): ${h.arab.join(' ')}`)
259
273
  for (const k of h.kabur) {
260
274
  console.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prpm-dbp",
3
- "version": "0.16.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
@@ -428,13 +455,15 @@ export async function keJawi(teks, cache, opts = {}) {
428
455
  export function keRumi(
429
456
  teksJawi,
430
457
  cache,
431
- { penanda = ['«', '»'], dataset = null, pilihLazim = false } = {},
458
+ { penanda = ['«', '»'], dataset = null, pilihLazim = false, padanRangka = false } = {},
432
459
  ) {
433
460
  const peta = petaSongsang(cache, dataset)
434
461
  const tidakDikenali = []
435
462
  const kabur = []
436
463
  const arab = []
437
464
  const sendiDipisah = []
465
+ const bukanPiawai = []
466
+ const rangka = padanRangka ? petaRangka(cache, dataset) : null
438
467
  const bahagian = teksJawi.split(PECAH_JAWI)
439
468
  const keluar = []
440
469
 
@@ -491,6 +520,22 @@ export function keRumi(
491
520
  }
492
521
  if (dipisah) { keluar.push(dipisah); sendiDipisah.push(bahagian[i]); continue }
493
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
+
494
539
  tidakDikenali.push(bahagian[i])
495
540
  keluar.push(penanda[0] + bahagian[i] + penanda[1])
496
541
  continue
@@ -521,6 +566,7 @@ export function keRumi(
521
566
  kabur,
522
567
  arab: [...new Set(arab)],
523
568
  sendiDipisah,
569
+ ejaanBukanPiawai: bukanPiawai,
524
570
  saizPeta: peta.size,
525
571
  lengkap: tidakDikenali.length === 0,
526
572
  }