prpm-dbp 0.19.0 → 0.20.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
@@ -43,6 +43,35 @@ Empat perkara yang ditangkap dalam teks Jawi:
43
43
 
44
44
  Ruang tersasar itu kes paling berbahaya. Separuh perkataan boleh **berjaya secara palsu**: `ݢول` benar-benar memetakan kepada dua perkataan DBP tulen, `gol` dan `gul`. Penukar yang memilih calon paling kerap akan mengeluarkan "gol" dengan yakin, dan separuh perkataan hilang tanpa sesiapa perasan. Sebab itu percantuman dicuba apabila **salah satu** jiran belum selesai, bukan kedua-duanya.
45
45
 
46
+ ## Teks Arab dan al-Quran TIDAK PERNAH ditukar
47
+
48
+ Pedoman §11.1: petikan al-Quran, hadis, nama surah, nama tokoh dan nama tempat **kekal ejaan Arab asalnya**. Ia tidak ditransliterasi ke Rumi dan tidak pernah dicadangkan pembetulan ejaan Melayu.
49
+
50
+ Ini bukan teori. `الدين` (ad-Din) memetakan kepada dua entri dalam data, jadi penukar lama mengeluarkan `addin|al-din` untuk teks Arab tulen dan pembaca tidak nampak apa-apa salah.
51
+
52
+ Dua isyarat automatik, kedua-duanya diukur pada 66,323 ejaan Jawi DBP sebenar:
53
+
54
+ | Isyarat | Padanan palsu | Contoh dilindungi |
55
+ |---|---|---|
56
+ | tanda baris (harakat) | **0** | `إِنَّ ٱللَّهَ مَعَ ٱلصَّٰبِرِينَ` |
57
+ | `أ` atau `آ` di awal | 5 (0.008%) | `أصحاب` |
58
+ | `ال` di awal, tidak dikenali | — | `اليمين`, `المقربون`, `الفاتحة` |
59
+
60
+ Petikan al-Quran datang bertanda baris dalam amalan biasa, jadi ia dilindungi tanpa apa-apa persediaan.
61
+
62
+ **Istilah Arab telanjang** — yang tiada tanda baris dan padan sesuatu dalam kamus, seperti `الدين` — diisytihar sekali:
63
+
64
+ ```bash
65
+ npx prpm-dbp nama ad-din الدين --jenis arab
66
+ ```
67
+
68
+ Selepas itu ia tidak disentuh lagi, dalam `rumi` mahupun `gate`.
69
+
70
+ **Dua perkara yang sengaja TIDAK dibuat**, kedua-duanya diuji dan ditolak:
71
+
72
+ 1. **Rentang Arab tidak merebak kepada jiran.** Kelihatan betul sehingga diuji: satu ejaan Melayu yang salah di sebelah petikan al-Quran tidak dapat dibezakan daripada satu perkataan Arab di sebelah petikan al-Quran, jadi `منجاوهكن` diserap dan kesilapan sebenar hilang. Mendiamkan ralat tulen lebih buruk daripada menandakan satu perkataan Arab yang jarang.
73
+ 2. **Jaminan DBP tidak dituntut untuk mengatasi isyarat `ال`.** Ia membekukan 325 daripada 346 perkataan ber-`ال`, termasuk `alah`, `alaf` dan `alabaster`, yang alif-lamnya sebahagian perkataan dan bukan kata sandang. Ortografi sahaja tidak dapat membezakannya.
74
+
46
75
  Nama khas Arab tanpa kata sandang `ال` (`أصحاب`, nama tokoh, nama tempat) tidak dapat disahkan kamus Melayu. Isytiharkannya melalui `npx prpm-dbp nama <rumi> <jawi> --jenis arab`; selepas itu ia berhenti menjadi ralat, tetapi tetap **bukan** lulus DBP.
47
76
 
48
77
  `gate` keluar dengan kod 1 apabila ada ralat, jadi ia boleh dipasang sebagai hook atau langkah CI. Peraturan yang bergantung pada agen ingat sendiri akan gagal, dan gagal secara senyap.
package/bin/cli.js CHANGED
@@ -7,6 +7,17 @@ import { banding, cariKamus } from '../src/kamus.js'
7
7
  import { keJawi, keRumi, semakJawi } from '../src/tukar.js'
8
8
  import { huraiTsv, muatDataset, simpanDataset } from '../src/dataset.js'
9
9
  import { NAMA_PATH, muatNama, tambahNama } from '../src/nama.js'
10
+
11
+ // Istilah Arab yang pengguna sahkan sendiri. Petikan al-Quran datang
12
+ // bertanda baris dalam amalan biasa dan dikenali automatik; istilah
13
+ // telanjang seperti الدين diisytihar sekali di sini dan tidak pernah
14
+ // ditransliterasi lagi, dalam `rumi` mahupun `gate`.
15
+ const arabDiisytiharkan = async () =>
16
+ new Set(
17
+ Object.values(await muatNama())
18
+ .filter((n) => n?.jenis === 'arab' && n.jawi)
19
+ .map((n) => n.jawi),
20
+ )
10
21
  import { gate, kodKeluar } from '../src/gate.js'
11
22
  import {
12
23
  CACHE_PATH,
@@ -252,6 +263,7 @@ async function main() {
252
263
  pilihLazim: pilihan.pilih,
253
264
  padanRangka: pilihan.rangka,
254
265
  glosari: pilihan.glosari ? JSON.parse(await readFile(pilihan.glosari, 'utf8')) : null,
266
+ arabDiisytihar: await arabDiisytiharkan(),
255
267
  })
256
268
  if (pilihan.json) { keluar(h); break }
257
269
  console.log(h.teks)
@@ -330,6 +342,7 @@ async function main() {
330
342
  .map((n) => n?.jawi)
331
343
  .filter(Boolean),
332
344
  ),
345
+ arabDiisytihar: await arabDiisytiharkan(),
333
346
  luarTalian: pilihan.luarTalian !== false,
334
347
  abai,
335
348
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prpm-dbp",
3
- "version": "0.19.0",
3
+ "version": "0.20.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/gate.js CHANGED
@@ -29,7 +29,7 @@ import { cariDataset } from './dataset.js'
29
29
  import { normalisasiJawi } from './kamus.js'
30
30
  import { jarakHuruf, petaSongsang } from './tukar.js'
31
31
  import { pisahUlangJawi } from './pedoman.js'
32
- import { rangkaKonsonan } from './semak.js'
32
+ import { isyaratArab, rangkaKonsonan } from './semak.js'
33
33
 
34
34
  const TOKEN_RUMI = /([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ'-]*)/g
35
35
  const TOKEN_JAWI = /([؀-ۿݐ-ݿﭐ-﷿ﹰ-]+)/g
@@ -55,7 +55,7 @@ const HARAKAT = /[ً-ْٰٓ-ٕ]/
55
55
  * kerana separuh pertama "berjaya".
56
56
  */
57
57
  export function gateJawi(teks, cache, opts = {}) {
58
- const { dataset = null, abai = new Set(), namaJawi = new Set() } = opts
58
+ const { dataset = null, abai = new Set(), namaJawi = new Set(), arabDiisytihar = new Set() } = opts
59
59
  const peta = petaSongsang(cache, dataset)
60
60
 
61
61
  // Indeks rangka konsonan, dibina sekali. Ejaan yang tersalah biasanya
@@ -113,21 +113,48 @@ export function gateJawi(teks, cache, opts = {}) {
113
113
  return m ? m.layak.length === 1 : false
114
114
  }
115
115
 
116
+ // ── Kesan rentang Arab SEBELUM apa-apa dinilai ────────────────────────
117
+ //
118
+ // Pedoman §11.1: petikan al-Quran, hadis, nama surah, nama tokoh dan nama
119
+ // tempat KEKAL ejaan Arab asalnya. Ia tidak boleh disemak dengan kamus
120
+ // Melayu, dan lebih penting lagi ia tidak boleh DICADANGKAN pembetulan.
121
+ // Gate hanya melapor, tetapi agen yang membaca "guna اصحاب" akan menyunting
122
+ // ayat al-Quran. Mencadangkan pembetulan pada teks wahyu lebih teruk
123
+ // daripada tidak menyemaknya langsung.
124
+ //
125
+ // Diukur pada 66,323 ejaan Jawi DBP sebenar:
126
+ // harakat 0 padanan -> isyarat sempurna
127
+ // `أ` di awal 4 padanan -> 0.006%, dan keempat-empatnya larut sendiri
128
+ // `إ` di awal 7 padanan -> termasuk إسلام, jadi BUKAN isyarat
129
+ // `ال` di awal 360 padanan -> hanya berguna bersama syarat tidak larut
130
+ const benihArab = (t) => isyaratArab(t, (x) => Boolean(cari(x)), arabDiisytihar)
131
+
132
+ const arab = new Set()
133
+ for (const tok of token) if (benihArab(tok.teks)) arab.add(tok.i)
134
+
135
+ // Rentang Arab SENGAJA TIDAK merebak kepada jiran. Percubaan merebak
136
+ // melalui ruang kepada token yang tidak larut kelihatan betul sehingga
137
+ // diuji: satu ejaan Melayu yang salah di sebelah petikan al-Quran tidak
138
+ // dapat dibezakan daripada satu perkataan Arab di sebelah petikan
139
+ // al-Quran, jadi منجاوهكن diserap dan kesilapan sebenar hilang.
140
+ //
141
+ // Mendiamkan ralat tulen lebih buruk daripada menandakan satu perkataan
142
+ // Arab yang jarang. Setiap token mesti membawa isyaratnya sendiri.
143
+ // Petikan al-Quran datang bertanda baris dalam amalan biasa, jadi ia
144
+ // dilindungi; apa yang tidak, diisytihar melalui `nama` atau `--abai`.
145
+
116
146
  for (const [n, tok] of token.entries()) {
117
147
  const t = tok.teks
118
148
  if (abai.has(t) || abai.has(normalisasiJawi(t))) continue
119
149
 
120
- // Petikan Arab tidak selalu bertanda baris. Kata sandang `ال` ialah
121
- // penanda paling boleh dipercayai yang tinggal: ia membuka hampir setiap
122
- // frasa Arab yang dipetik (اليمين, المقربون, الشمال) dan pada dasarnya
123
- // tidak pernah membuka kata Melayu Jawi. Nama khas Arab tanpa kata
124
- // sandang (أصحاب) tetap perlu diisytihar melalui simpanan `nama`;
125
- // kamus Melayu memang tidak boleh mengesahkannya, dan berpura-pura
126
- // boleh adalah jaminan palsu.
127
- if (HARAKAT.test(t) || (/^ال/.test(t) && !cari(t))) {
128
- // Pedoman §11.1: petikan Arab kekalkan ejaan asal. Ia bukan lulus DBP
129
- // kerana kamus Melayu tidak boleh mengesahkannya; ia keadaan ketiga.
130
- if (!dilihat.has(t)) { dilihat.add(t); tidakDapatDisahkan.push({ kata: t, sebab: 'teks Arab, dikekalkan (Pedoman §11.1)' }) }
150
+ if (arab.has(tok.i)) {
151
+ // Bukan lulus DBP: kamus Melayu tidak mengesahkannya. Bukan ralat:
152
+ // ejaan Arab asal memang betul. Ia keadaan ketiga, dan TIADA cadangan
153
+ // pembetulan dikeluarkan untuknya.
154
+ if (!dilihat.has(t)) {
155
+ dilihat.add(t)
156
+ tidakDapatDisahkan.push({ kata: t, sebab: 'teks Arab, kekalkan seadanya (Pedoman §11.1)' })
157
+ }
131
158
  continue
132
159
  }
133
160
 
@@ -136,7 +163,7 @@ export function gateJawi(teks, cache, opts = {}) {
136
163
  // Cuba cantum dengan jiran SEBELUM melaporkan apa-apa, kerana ruang
137
164
  // tersasar mencemarkan kedua-dua separuh sekali gus.
138
165
  const seterusnya = token[n + 1]
139
- if (seterusnya && ruangSahaja(tok.i, seterusnya.i)) {
166
+ if (seterusnya && !arab.has(seterusnya.i) && ruangSahaja(tok.i, seterusnya.i)) {
140
167
  const belumSelesai = !selesai(t) || !selesai(seterusnya.teks)
141
168
  if (belumSelesai) {
142
169
  const gabung = cari(t + seterusnya.teks)
package/src/semak.js CHANGED
@@ -152,3 +152,52 @@ export function rangkaKonsonan(jawi) {
152
152
  if (keluar && HUJUNG_SETARA.has(keluar.slice(-1))) keluar = keluar.slice(0, -1) + 'ه'
153
153
  return keluar
154
154
  }
155
+
156
+ // ── Pengesan teks Arab ────────────────────────────────────────────────────
157
+ //
158
+ // Pedoman DBP §11.1: petikan al-Quran, hadis, nama surah, nama tokoh dan
159
+ // nama tempat KEKAL ejaan Arab asalnya. Ia tidak boleh ditransliterasi ke
160
+ // Rumi dan tidak boleh dicadangkan pembetulan ejaan Melayu.
161
+ //
162
+ // Ini bukan sekadar teori. `الدين` (ad-Din) memetakan kepada dua entri Jawi
163
+ // dalam data DBP, jadi tanpa pemeriksaan ini penukar mengeluarkan
164
+ // "addin|al-din" untuk teks Arab dan pembaca tidak nampak apa-apa salah.
165
+ //
166
+ // Diukur pada 66,323 ejaan Jawi DBP sebenar:
167
+ // harakat 0 padanan -> isyarat sempurna
168
+ // `أ` `آ` awal 5 padanan -> 0.008%
169
+ // `ال` awal 360 padanan -> berguna HANYA bersama syarat tidak larut
170
+ // `إ` awal 7 padanan -> termasuk إسلام, jadi BUKAN isyarat
171
+ const HARAKAT = /[ً-ْٰٓ-ٕ]/
172
+
173
+ /**
174
+ * @param {string} token satu perkataan Jawi/Arab
175
+ * @param {(t: string) => boolean} larut adakah token ini dikenali sebagai
176
+ * kata Melayu langsung
177
+ * @param {Set<string>} diisytihar istilah Arab yang pengguna sahkan sendiri
178
+ *
179
+ * Menuntut jaminan DBP lapisan 1 untuk mengatasi isyarat `ال` telah diuji
180
+ * dan DITOLAK: ia membekukan 325 daripada 346 perkataan ber-`ال`, termasuk
181
+ * alah, alaf dan alabaster, yang alif-lamnya sebahagian perkataan dan bukan
182
+ * kata sandang. Ortografi sahaja tidak dapat membezakannya.
183
+ *
184
+ * Yang tinggal ialah dua isyarat yang benar-benar bersih, ditambah
185
+ * pengisytiharan pengguna. Petikan al-Quran datang bertanda baris dalam
186
+ * amalan biasa; istilah Arab telanjang seperti `الدين` diisytihar sekali
187
+ * melalui simpanan `nama --jenis arab` dan tidak pernah disentuh lagi.
188
+ *
189
+ * Isyarat sengaja per-token dan TIDAK merebak kepada jiran. Percubaan
190
+ * merebak melalui ruang kelihatan betul sehingga diuji: satu ejaan Melayu
191
+ * yang salah di sebelah petikan al-Quran tidak dapat dibezakan daripada satu
192
+ * perkataan Arab di sebelah petikan al-Quran, jadi ralat tulen diserap dan
193
+ * hilang. Mendiamkan kesilapan sebenar lebih buruk daripada menandakan satu
194
+ * perkataan Arab yang jarang.
195
+ */
196
+ export function isyaratArab(token, larut = () => false, diisytihar = new Set()) {
197
+ if (HARAKAT.test(token)) return 'harakat'
198
+ if (diisytihar.has(token)) return 'diisytihar pengguna'
199
+ if (larut(token)) return null
200
+ if (/^[أآ]/.test(token)) return 'hamzah awal'
201
+ if (/^ال/.test(token)) return 'kata sandang'
202
+ return null
203
+ }
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 { rangkaKonsonan, semakHurufAkhir } from './semak.js'
15
+ import { isyaratArab, rangkaKonsonan, semakHurufAkhir } from './semak.js'
16
16
  import {
17
17
  imbuhAkhiranAn,
18
18
  imbuhAkhiranKan,
@@ -491,6 +491,7 @@ export function keRumi(
491
491
  pilihLazim = false,
492
492
  padanRangka = false,
493
493
  glosari = null,
494
+ arabDiisytihar = new Set(),
494
495
  } = {},
495
496
  ) {
496
497
  const peta = petaSongsang(cache, dataset)
@@ -521,9 +522,17 @@ export function keRumi(
521
522
  }
522
523
  }
523
524
 
524
- // Teks Arab bertanda baris dibiarkan seadanya. Cuba menukarnya akan
525
- // merosakkan petikan al-Quran dan hadis dalam dokumen Melayu.
526
- if (HARAKAT.test(bahagian[i])) {
525
+ // Teks Arab dibiarkan seadanya. Cuba menukarnya akan merosakkan petikan
526
+ // al-Quran dan hadis dalam dokumen Melayu.
527
+ //
528
+ // Tanda baris sahaja TIDAK memadai. Frasa seperti أصحاب اليمين datang
529
+ // tanpa baris, dan lebih buruk lagi `الدين` memetakan kepada dua entri
530
+ // Jawi dalam data DBP — jadi penukar mengeluarkan "addin|al-din" untuk
531
+ // teks Arab tulen dan pembaca tidak nampak apa-apa salah. Perkataan
532
+ // yang benar-benar larut dalam DBP tidak terjejas: isyarat kata sandang
533
+ // hanya terpakai apabila perkataan itu bukan kata Melayu.
534
+ const tandaArab = isyaratArab(bahagian[i], (t) => peta.has(normalisasiJawi(t)), arabDiisytihar)
535
+ if (tandaArab) {
527
536
  arab.push(bahagian[i])
528
537
  keluar.push(bahagian[i])
529
538
  continue