prpm-dbp 0.14.0 → 0.16.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/README.md CHANGED
@@ -132,7 +132,9 @@ Pengguna kemudian menarik versi baharu dengan `npx skills update`.
132
132
  | `banding <kata...>` | silang semak PRPM lawan KamusDBP |
133
133
  | `cache` | status cache, `--buang <kata>` untuk padam satu |
134
134
 
135
- Pilihan: `--refresh`, `--had <n>`, `--delay <ms>`, `--json`, `--cepat`, `--pedoman`.
135
+ Pilihan: `--refresh`, `--had <n>`, `--delay <ms>`, `--json`, `--cepat`, `--pedoman`, `--luar-talian`, `--homograf`, `--konteks`.
136
+
137
+ Merumikan ialah keputusan peringkat ayat, bukan peringkat perkataan. `مڠيکوت` ialah ejaan sah bagi `mengikut` dan `mengekot` sekali gus, kedua-duanya entri PRPM sebenar. Gunakan `--konteks` supaya ayat dan makna setiap calon dipaparkan bersama, berserta bilangan rujukan korpus DBP yang mengisih calon lazim ke hadapan.
136
138
 
137
139
  `--luar-talian` tidak pernah menyentuh rangkaian. Wajib untuk laluan permintaan aplikasi: dokumen penuh dari cache mengambil 0.19 saat, manakala satu perkataan baharu daripada PRPM mengambil beberapa saat.
138
140
 
package/SKILL.md CHANGED
@@ -17,6 +17,36 @@ Model boleh hasilkan Jawi yang nampak betul tetapi salah, dan pembaca tidak dapa
17
17
 
18
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
19
 
20
+ 0d. **Merumikan ialah keputusan peringkat AYAT, bukan peringkat perkataan.** Jangan tukar token demi token dan anggap ia selesai. Ejaan Jawi menggunakan empat huruf vokal berbanding enam dalam Rumi, jadi satu ejaan kerap membawa lebih daripada satu bacaan sah. `مڠيکوت` ialah `mengikut` **dan** `mengekot`, dua-duanya entri PRPM sebenar dengan sebutan sendiri. Kamus mengesahkan kedua-duanya wujud; hanya ayat menentukan yang mana dimaksudkan.
21
+
22
+ Gunakan `--konteks` supaya ayat dan makna setiap calon dipaparkan bersama:
23
+
24
+ ```
25
+ مڠيکوت -> mengikut atau mengekot
26
+ ayat: itu mengikut|mengekot sukatan pelajaran.
27
+ mengikut 2439 rujukan pergi bersama-sama dgn, menurut, mengiring
28
+ mengekot 6 rujukan merapatkan paha dan tangan ke badan
29
+ ```
30
+
31
+ 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
+
33
+ 0e. **Pembahagian kerja: alat mengemukakan, ANDA memutuskan.** CLI tidak boleh membaca konteks; anda boleh. Jangan sesekali menghantar `mengikut|mengekot` sebagai hasil akhir kepada pengguna.
34
+
35
+ Aliran yang betul:
36
+
37
+ ```bash
38
+ npx prpm-dbp rumi teks.txt --pilih --konteks
39
+ ```
40
+
41
+ `--pilih` menghasilkan teks bersih dengan memilih calon paling lazim, dan menyenaraikan setiap pilihan itu:
42
+
43
+ ```
44
+ itu mengikut sukatan pelajaran.
45
+ dipilih ikut kekerapan (SEMAK konteks): مڠيکوت -> mengikut [lain: mengekot]
46
+ ```
47
+
48
+ 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.
49
+
20
50
  1. **Jangan hantar Jawi yang belum disemak.** Setiap perkataan Jawi dalam output mesti datang dari PRPM atau ditanda belum disahkan.
21
51
  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.
22
52
  3. **Sebut sumber.** PRPM ada beberapa kamus. Bila petik definisi, sebut kamus mana (Kamus Dewan Edisi Keempat, Kamus Pelajar Edisi Kedua, dan lain-lain).
package/bin/cli.js CHANGED
@@ -37,6 +37,9 @@ Pilihan: --refresh --had <n> --delay <ms> --json --cepat
37
37
  --pedoman jana imbuhan ikut peraturan DBP (88-98% tepat, hasil ditanda)
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
+ --konteks papar ayat dan makna setiap calon bagi kekaburan (rumi)
41
+ --pilih pilih calon paling lazim supaya teks boleh terus dibaca;
42
+ setiap pilihan disenaraikan untuk disemak ikut konteks
40
43
 
41
44
  Cache: ${CACHE_PATH}
42
45
  `
@@ -53,6 +56,8 @@ function huraiArgs(argv) {
53
56
  else if (a === '--pedoman') pilihan.pedoman = true
54
57
  else if (a === '--luar-talian') pilihan.luarTalian = true
55
58
  else if (a === '--homograf') pilihan.homograf = true
59
+ else if (a === '--konteks') pilihan.konteks = true
60
+ else if (a === '--pilih') pilihan.pilih = true
56
61
  else if (a === '--jenis') pilihan.jenis = argv[++i]
57
62
  else if (a === '--had') pilihan.had = Number(argv[++i])
58
63
  else if (a === '--delay') pilihan.delay = Number(argv[++i])
@@ -218,15 +223,46 @@ async function main() {
218
223
 
219
224
  case 'rumi': {
220
225
  const teks = await readFile(baki[0], 'utf8')
221
- const h = keRumi(teks, cache, { dataset: await muatDataset() })
226
+ const h = keRumi(teks, cache, {
227
+ dataset: await muatDataset(),
228
+ pilihLazim: pilihan.pilih,
229
+ })
222
230
  if (pilihan.json) { keluar(h); break }
223
231
  console.log(h.teks)
224
232
  console.error(`\npeta songsang: ${h.saizPeta} ejaan`)
225
233
  if (h.tidakDikenali.length) {
226
234
  console.error(`tidak dikenali (${h.tidakDikenali.length}): ${h.tidakDikenali.join(', ')}`)
227
235
  }
236
+ if (pilihan.konteks && h.kabur.length) {
237
+ // Kekaburan hanya boleh diselesaikan oleh ayat, bukan oleh kamus.
238
+ // Papar ayat penuh berserta makna setiap calon supaya keputusan
239
+ // itu boleh dibuat tanpa membuka PRPM satu per satu.
240
+ const ayat = h.teks.split(/(?<=[.!?])\s+/)
241
+ console.error('\n--- konteks untuk kekaburan ---')
242
+ for (const k of h.kabur) {
243
+ const pilihanTeks = k.calon.join('|')
244
+ const induk = ayat.find((a) => a.includes(pilihanTeks)) ?? ''
245
+ console.error(`\n${k.jawi} -> ${k.calon.join(' atau ')}`)
246
+ if (induk) console.error(` ayat: ${induk.trim()}`)
247
+ for (const c of k.calon) {
248
+ const rec = cache[c]
249
+ const def = (rec?.entri?.[0]?.definisi ?? '').slice(0, 70)
250
+ const kerap = rec?.rujukanKorpus
251
+ console.error(
252
+ ` ${c.padEnd(12)} ${kerap != null ? String(kerap).padStart(5) + ' rujukan' : ' kekerapan tidak diketahui'}` +
253
+ (def ? ` ${def}` : ''),
254
+ )
255
+ }
256
+ }
257
+ }
228
258
  if (h.arab.length) console.error(`teks Arab dibiarkan (${h.arab.length}): ${h.arab.join(' ')}`)
229
- for (const k of h.kabur) console.error(`kabur: ${k.jawi} -> ${k.calon.join(', ')}`)
259
+ for (const k of h.kabur) {
260
+ console.error(
261
+ k.dipilih
262
+ ? `dipilih ikut kekerapan (SEMAK konteks): ${k.jawi} -> ${k.dipilih} [lain: ${k.calon.slice(1).join(', ')}]`
263
+ : `kabur: ${k.jawi} -> ${k.calon.join(', ')}`,
264
+ )
265
+ }
230
266
  break
231
267
  }
232
268
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prpm-dbp",
3
- "version": "0.14.0",
3
+ "version": "0.16.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
@@ -34,6 +34,15 @@ const DEFINISI = /<b>Definisi\s*:\s*<\/b>([\s\S]*?)(?=<b>\(|$)/
34
34
  const SUMBER = /<b>\(([^)]+)\)<\/b>/
35
35
  const TIADA_TESAURUS = /Tiada maklumat tesaurus untuk kata/
36
36
 
37
+ // Panel "Juga ditemukan dalam" menyenaraikan bilangan rujukan perkataan itu
38
+ // dalam setiap korpus DBP. Jumlahnya ialah isyarat kekerapan yang BERSUMBER,
39
+ // bukan tekaan: mengikut 2439 rujukan berbanding mengekot 6.
40
+ //
41
+ // Digunakan untuk MENGISIH calon yang kabur, bukan untuk memilih. Perkataan
42
+ // jarang tetap betul apabila ayat menghendakinya.
43
+ const PANEL_KORPUS = /Juga ditemukan dalam:([\s\S]*?)Perkhidmatan/
44
+ const KIRAAN_KORPUS = /<i>\((\d+)\)<\/i>/g
45
+
37
46
  const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'", nbsp: ' ' }
38
47
 
39
48
  export function stripTags(raw) {
@@ -65,11 +74,17 @@ export function parse(kata, page) {
65
74
  })
66
75
  }
67
76
 
77
+ const panel = page.match(PANEL_KORPUS)
78
+ const rujukanKorpus = panel
79
+ ? [...panel[1].matchAll(KIRAAN_KORPUS)].reduce((jum, m) => jum + Number(m[1]), 0)
80
+ : null
81
+
68
82
  const semuaJawi = entri.map((e) => e.jawi).filter(Boolean)
69
83
  const unik = [...new Set(semuaJawi)]
70
84
  return {
71
85
  kata,
72
86
  dijumpai: entri.length > 0,
87
+ rujukanKorpus,
73
88
  jawi: unik[0] ?? null,
74
89
  jawiVarian: unik.slice(1),
75
90
  entri,
package/src/tukar.js CHANGED
@@ -226,10 +226,21 @@ export function petaSongsang(cache, dataset = null) {
226
226
  * Apabila tiada calon lapisan 1, calon yang belum diketahui digunakan dan
227
227
  * sumbernya dilabel, tidak dipulangkan secara senyap.
228
228
  */
229
- export function pilihCalon(masuk) {
229
+ export function pilihCalon(masuk, cache = null) {
230
230
  if (!masuk) return null
231
231
  if (Array.isArray(masuk)) return { calon: masuk, sumber: 'campur' }
232
- if (masuk.layak?.length) return { calon: masuk.layak, sumber: 'dbp' }
232
+
233
+ // Isih ikut kekerapan korpus DBP apabila diketahui: calon yang lebih
234
+ // lazim didahulukan supaya pembaca melihatnya dahulu. Ini MENGISIH,
235
+ // bukan memilih; semua calon tetap dipulangkan.
236
+ const isih = (senarai) => {
237
+ if (!cache || senarai.length < 2) return senarai
238
+ return [...senarai].sort(
239
+ (a, b) => (cache[b]?.rujukanKorpus ?? 0) - (cache[a]?.rujukanKorpus ?? 0),
240
+ )
241
+ }
242
+
243
+ if (masuk.layak?.length) return { calon: isih(masuk.layak), sumber: 'dbp' }
233
244
  if (masuk.entah?.length) return { calon: masuk.entah, sumber: 'dataset' }
234
245
  if (masuk.tolak?.length) return { calon: masuk.tolak, sumber: 'rujukan' }
235
246
  return null
@@ -414,7 +425,11 @@ export async function keJawi(teks, cache, opts = {}) {
414
425
  * menggugurkannya. Kerana itu perlanggaran jauh lebih jarang daripada yang
415
426
  * disangka. Bila ia berlaku, SEMUA calon dilaporkan, bukan dipilih satu.
416
427
  */
417
- export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null } = {}) {
428
+ export function keRumi(
429
+ teksJawi,
430
+ cache,
431
+ { penanda = ['«', '»'], dataset = null, pilihLazim = false } = {},
432
+ ) {
418
433
  const peta = petaSongsang(cache, dataset)
419
434
  const tidakDikenali = []
420
435
  const kabur = []
@@ -452,7 +467,7 @@ export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null
452
467
  }
453
468
  if (!bolehGabung) continue
454
469
  const frasa = bahagian.slice(i, akhir + 1).join(' ').replace(/\s+/g, ' ')
455
- const jumpa = pilihCalon(peta.get(normalisasiJawi(frasa)))
470
+ const jumpa = pilihCalon(peta.get(normalisasiJawi(frasa)), cache)
456
471
  if (jumpa) { padanan = jumpa.calon; hujung = akhir; break }
457
472
  }
458
473
 
@@ -483,8 +498,19 @@ export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null
483
498
 
484
499
  if (padanan.length === 1) keluar.push(padanan[0])
485
500
  else {
486
- kabur.push({ jawi: bahagian.slice(i, hujung + 1).join(''), calon: padanan })
487
- keluar.push(padanan.join('|'))
501
+ const rekod = { jawi: bahagian.slice(i, hujung + 1).join(''), calon: padanan }
502
+ kabur.push(rekod)
503
+ // pilihLazim menghasilkan teks yang boleh terus dibaca dengan memilih
504
+ // calon paling lazim dalam korpus DBP. Ia BUKAN pemahaman konteks:
505
+ // setiap pilihan direkod dalam medan `kabur` supaya agen atau manusia
506
+ // yang membaca ayat boleh menggantikannya. Kekerapan ialah tekaan
507
+ // terbaik tanpa konteks, bukan jawapan.
508
+ if (pilihLazim) {
509
+ rekod.dipilih = padanan[0]
510
+ keluar.push(padanan[0])
511
+ } else {
512
+ keluar.push(padanan.join('|'))
513
+ }
488
514
  }
489
515
  i = hujung
490
516
  }