dsh-paper-search 0.1.1 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  | dsh-paper-search | DeepSeek Harness | Diuji pada |
6
6
  |---|---|---|
7
- | 0.1.1 | 0.1.7-rc.2 | 28 Sep 2026, Windows, Node 24 |
7
+ | 0.1.3 | 0.1.7-rc.2 | 28 Sep 2026, Windows, Node 24 |
8
8
  | ~~0.1.0~~ | — | **JANGAN DIPAKAI — plugin gagal dimuat, lihat 0.1.1** |
9
9
 
10
10
  Paket ini tidak memakai API internal DSH selain `ctx.tools.register` dan
@@ -12,6 +12,74 @@ Paket ini tidak memakai API internal DSH selain `ctx.tools.register` dan
12
12
  berjalan di profil ini (`dsh-search-cascade`), jadi titik patahnya sama dan sudah
13
13
  diketahui.
14
14
 
15
+ ## [0.1.3] - 2026-09-28
16
+
17
+ ### Fixed
18
+
19
+ - **Portal Indonesia melaporkan GAGAL padahal hasilnya cuma KOSONG.** Ketiga
20
+ konektor Indonesia melempar `markup ... berubah` setiap kali penanda hasil tidak
21
+ ditemukan — termasuk saat portal itu **sah-sah saja menjawab nol hasil**. Itu
22
+ keliru dengan arah berlawanan dari prinsip paket ini: melaporkan kegagalan di
23
+ tempat yang seharusnya kosong sama menyesatkannya dengan menyembunyikan
24
+ kegagalan.
25
+
26
+ Ketahuan dari **uji orkestrasi**, bukan dari uji unit: `Sinta` gagal untuk
27
+ `"literasi digital remaja"` padahal berhasil untuk `"pendidikan karakter"`.
28
+ Setelah halaman kosong dan halaman ber-hasil dibandingkan, ternyata ketiga
29
+ portal punya penanda nol-hasilnya sendiri:
30
+
31
+ | Portal | Penanda nol hasil | Halaman kosong |
32
+ |---|---|---|
33
+ | Garuda | `Found 0 documents` | 11.181 huruf |
34
+ | Sinta | header `Results for "<q>"` tetap ada, tautan profil nol | 34.063 huruf |
35
+ | IOS | `No Results!` | 23.041 huruf |
36
+
37
+ `portalNolHasil()` sekarang membedakan **tiga** kemungkinan — nol hasil (→ `[]`),
38
+ markup berubah (→ error), dan diblokir (→ error) — bukan dua.
39
+
40
+ Akibatnya untuk Sinta: kueri spesifik yang memang nol hasil tidak lagi tampak
41
+ seperti kerusakan. Sinta mendaftar **jurnal**, bukan artikel, jadi nol hasil itu
42
+ wajar dan sekarang dilaporkan sebagai nol.
43
+
44
+ ### Added
45
+
46
+ - 3 asersi regresi: kueri omong kosong ke ketiga portal Indonesia harus
47
+ menghasilkan **0 hasil tanpa error**.
48
+
49
+ ## [0.1.2] - 2026-09-28
50
+
51
+ ### Fixed
52
+
53
+ - **Judul Garuda masih bisa terduplikasi.** Perbaikan di rilis sebelumnya baru
54
+ menangani pengulangan **persis** (`X X X`). Setelah mengukur **50 judul
55
+ sungguhan** (5 kueri, diambil apa adanya dari halaman Garuda), ternyata ada
56
+ **empat** bentuk pengulangan, bukan satu:
57
+
58
+ | Bentuk | Contoh | Panjang |
59
+ |---|---|---|
60
+ | A — periode persis | `X X X X X X` | 255 → 43 |
61
+ | B — diulang sesudah `": "` | `X: X` | 194 → 96 |
62
+ | C — awalan lalu `X: X` | `NLP, Machine Learning X: X` | 270 → 145 |
63
+ | D — frasa diulang di awal | `IMPLEMENTASI PENDIDIKAN KARAKTER IMPLEMENTASI …` | 306 → 295 |
64
+
65
+ `runtuhkanPeriodik()` diganti **`runtuhkanJudul()`** yang menangani keempatnya,
66
+ termasuk pengulangan dengan **kapitalisasi berbeda** (252 → 125) — Garuda
67
+ memang sering mengulang judul dalam huruf besar lalu huruf kecil.
68
+
69
+ **Yang paling penting, dan inilah yang diuji:** judul **sah** berformat
70
+ `Judul: subjudul`, `Judul: (studi kasus)`, dan `Judul: kata kunci` **tidak**
71
+ ikut terpotong. Karena itu aturannya menuntut bagian sesudah `": "` benar-benar
72
+ **sama** dengan akhiran bagian sebelumnya — bukan sekadar ada titik dua. Pada
73
+ 50 judul itu hasilnya: **14 diringkas, 36 dibiarkan, 0 cacat**, dan **15 judul
74
+ bertitik dua tetap utuh**.
75
+
76
+ ### Added
77
+
78
+ - **11 asersi regresi baru** di `test/uji-sumber.mjs`, memakai teks yang diambil
79
+ apa adanya dari Garuda. Lima di antaranya justru menjaga agar judul **sah**
80
+ tidak terpotong — tanpa itu, aturannya bisa "lulus" hanya dengan memotong
81
+ setiap judul yang bertitik dua.
82
+
15
83
  ## [0.1.1] - 2026-09-28
16
84
 
17
85
  ### Fixed
package/lib/sources.js CHANGED
@@ -74,26 +74,56 @@ function normDoi(doi) {
74
74
  }
75
75
 
76
76
  /**
77
- * Beberapa portal menulis judul di dalam elemen <xmp> dan MENGULANGNYA untuk
78
- * mengisi lebar tampilan. Terukur di Garuda 28 Sep 2026: judul
79
- * "PENDIDIKAN KARAKTER ANAK USIA SEKOLAH DASAR" (44 huruf) muncul sekitar enam
80
- * kali dalam satu <xmp> sehingga panjangnya 255 huruf.
77
+ * Garuda menulis judul di dalam elemen <xmp> dengan BEBERAPA bentuk pengulangan,
78
+ * dan mengulanginya untuk mengisi lebar tampilan. Keempatnya diukur 28 Sep 2026
79
+ * dari **50 judul sungguhan** (5 kueri):
81
80
  *
82
- * Fungsi ini mencari periode terkecil dan menyisakan SATU salinan. Ia hanya
83
- * bertindak bila pengulangannya PERSIS dari awal sampai akhir — kalau tidak,
84
- * teks dibiarkan apa adanya. Sengaja konservatif: lebih baik judul berlebihan
85
- * daripada judul yang terpotong salah.
81
+ * A. periode persis "X X X X X X" (255 huruf)
82
+ * B. diulang sesudah ": " "X: X" (194 huruf)
83
+ * C. awalan lalu "X: X" "NLP, Machine Learning X: X" (270 huruf)
84
+ * D. frasa diulang di awal "IMPLEMENTASI PENDIDIKAN KARAKTER IMPLEMENTASI …"
85
+ *
86
+ * Yang TIDAK boleh disentuh — dan sudah diperiksa pada 50 sampel itu: judul SAH
87
+ * berformat "Judul: subjudul", "Judul: (studi kasus)", dan "Judul: kata kunci".
88
+ * Karena itu aturan (c) menuntut bagian sesudah ": " benar-benar SAMA dengan
89
+ * akhiran bagian sebelumnya — bukan sekadar ada titik dua. Pada 50 sampel,
90
+ * tidak satu pun judul sah ikut terpotong.
91
+ *
92
+ * Sengaja konservatif: lebih baik judul berlebihan daripada judul terpotong salah.
86
93
  */
87
- function runtuhkanPeriodik(s) {
88
- const t = (s ?? '').trim();
89
- const n = t.length;
90
- if (n < 30) return t;
91
- for (let p = 10; p <= Math.floor(n / 2); p++) {
92
- let cocok = true;
93
- for (let i = p; i < n; i++) {
94
- if (t[i] !== t[i % p]) { cocok = false; break; }
94
+ export function runtuhkanJudul(s) {
95
+ let t = (s ?? '').trim();
96
+ for (let putaran = 0; putaran < 4; putaran++) {
97
+ const sebelum = t;
98
+
99
+ // (A) periode persis: cari periode terkecil, sisakan satu salinan.
100
+ const n = t.length;
101
+ for (let p = 10; p <= Math.floor(n / 2); p++) {
102
+ let cocok = true;
103
+ for (let i = p; i < n; i++) {
104
+ if (t[i] !== t[i % p]) { cocok = false; break; }
105
+ }
106
+ if (cocok) { t = t.slice(0, p).trim(); break; }
107
+ }
108
+
109
+ // (D) frasa yang diulang di awal: "A A sisa" -> "A sisa".
110
+ // `A` harus berakhir di batas kata, supaya tidak memotong di tengah kata.
111
+ for (let p = 8; p <= Math.floor(t.length / 2); p++) {
112
+ const a = t.slice(0, p);
113
+ if (!/[\s.)\]]$/.test(a)) continue;
114
+ if (t.startsWith(a + a)) { t = (a + t.slice(2 * p)).trim(); break; }
95
115
  }
96
- if (cocok) return t.slice(0, p).trim();
116
+
117
+ // (B)/(C) diulang sesudah ": " terakhir — dibandingkan TANPA peduli huruf
118
+ // besar/kecil, karena Garuda sering mengulang judul dengan kapitalisasi beda.
119
+ const pisah = t.lastIndexOf(': ');
120
+ if (pisah > 0) {
121
+ const ekor = t.slice(pisah + 2).trim();
122
+ const kepala = t.slice(0, pisah).trim();
123
+ if (ekor.length >= 20 && kepala.toLowerCase().endsWith(ekor.toLowerCase())) t = kepala;
124
+ }
125
+
126
+ if (t === sebelum) break;
97
127
  }
98
128
  return t;
99
129
  }
@@ -101,7 +131,7 @@ function runtuhkanPeriodik(s) {
101
131
  function buatPaper(p) {
102
132
  const doi = normDoi(p.doi);
103
133
  return {
104
- title: potong(runtuhkanPeriodik(bersihTeks(p.title) ?? ''), 300),
134
+ title: potong(runtuhkanJudul(bersihTeks(p.title) ?? ''), 300),
105
135
  authors: Array.isArray(p.authors) ? p.authors.map(bersihTeks).filter(Boolean).slice(0, 15) : [],
106
136
  year: Number.isFinite(Number(p.year)) && Number(p.year) > 1500 ? Number(p.year) : null,
107
137
  source: p.source,
@@ -429,6 +459,29 @@ function ambilHtml(html, pola, batas) {
429
459
  return out;
430
460
  }
431
461
 
462
+ /**
463
+ * Saat penanda hasil tidak ditemukan, ada TIGA kemungkinan yang BERBEDA:
464
+ * 1. portal menjawab "nol hasil" -> `[]`, itu hasil yang SAH
465
+ * 2. markup portal berubah -> error
466
+ * 3. portal memblokir/mengalihkan -> error
467
+ *
468
+ * Sebelum ini (1) dan (2) diperlakukan sama, sehingga portal yang benar-benar
469
+ * kosong dilaporkan sebagai **GAGAL**. Itu keliru dengan arah berlawanan:
470
+ * melaporkan kegagalan padahal hasilnya sah-sah saja kosong — sama
471
+ * menyesatkannya dengan menyembunyikan kegagalan.
472
+ *
473
+ * Penanda nol-hasil diukur 28 Sep 2026 memakai kueri omong kosong, lalu
474
+ * dibandingkan dengan kueri yang ada hasilnya:
475
+ *
476
+ * Garuda : "Found 0 documents" (halaman 11.181 huruf)
477
+ * Sinta : header `Results for "<q>"` tetap ada, tautan profil nol
478
+ * (halaman 34.063 huruf) — Sinta TIDAK menulis pesan kosong
479
+ * IOS : "No Results!" (halaman 23.041 huruf)
480
+ */
481
+ function portalNolHasil(html, ...penanda) {
482
+ return penanda.some((re) => re.test(html));
483
+ }
484
+
432
485
  const GARUDA_BASE = 'https://garuda.kemdiktisaintek.go.id';
433
486
 
434
487
  /** Garuda — indeks artikel jurnal Indonesia (Kemdiktisaintek). */
@@ -442,9 +495,10 @@ async function garuda(query, { rows }) {
442
495
  journal: 'Garuda (jurnal Indonesia)',
443
496
  openAccess: true,
444
497
  })).filter((p) => p.title);
445
- // Penguraian HTML yang tidak menemukan satu pun pola = kemungkinan markup
446
- // berubah. Laporkan sebagai error, JANGAN sebagai "tidak ada hasil".
498
+ // Penguraian HTML yang tidak menemukan satu pun pola bisa berarti DUA hal yang
499
+ // berlawanan. Bedakan: Garuda menulis "Found 0 documents" saat memang kosong.
447
500
  if (hasil.length === 0 && !/title-article/.test(html)) {
501
+ if (portalNolHasil(html, /Found\s+0\s+document/i)) return [];
448
502
  throw new Error('markup Garuda berubah: penanda "title-article" tidak ditemukan lagi');
449
503
  }
450
504
  return hasil;
@@ -461,7 +515,12 @@ async function sinta(query, { rows }) {
461
515
  journal: 'Sinta (jurnal terakreditasi)',
462
516
  openAccess: true,
463
517
  })).filter((p) => p.title);
518
+ // Sinta TIDAK menulis pesan kosong — ia tetap menampilkan header
519
+ // `Results for "<kueri>"` lalu daftar yang kosong. Jadi header itu yang
520
+ // dipakai sebagai bukti "halaman dimuat, hasilnya memang nol".
521
+ // (Sinta mendaftar JURNAL, bukan artikel, jadi kueri spesifik memang sering nol.)
464
522
  if (hasil.length === 0 && !/journals\/profile\//.test(html)) {
523
+ if (portalNolHasil(html, /Results for/i)) return [];
465
524
  throw new Error('markup Sinta berubah: tautan "journals/profile" tidak ditemukan lagi');
466
525
  }
467
526
  return hasil;
@@ -479,6 +538,7 @@ async function ios(query, { rows }) {
479
538
  openAccess: true,
480
539
  })).filter((p) => p.title);
481
540
  if (hasil.length === 0 && !/class="title"/.test(html)) {
541
+ if (portalNolHasil(html, /No Results!/i)) return [];
482
542
  throw new Error('markup IOS berubah: tautan hasil ber-class "title" tidak ditemukan lagi');
483
543
  }
484
544
  return hasil;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-paper-search",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Literature search for DeepSeek Harness: thirteen international sources (Crossref, OpenAlex, DOAJ, Europe PMC, PubMed, PubMed Central, arXiv, Semantic Scholar, CORE, Zenodo, HAL, IACR ePrint, Unpaywall) and three Indonesian journal portals (Garuda, Sinta, IOS OneSearch), all native HTTP with no external CLI required, plus a status tool that reports which sources answer and which credentials are configured.",
5
5
  "license": "MIT",
6
6
  "type": "module",