diff --git a/CHANGELOG.md b/CHANGELOG.md index c54a76c..b951d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.1.18 — 2026-10-05 + +Rilis fitur Wave 0.1.18 untuk penambahan modul `penulis-ilmiah` melalui eksepsi unfreeze resmi katalog (Option B: 65 → 66 skill, 51 model-invoked + 15 manual, tanpa pensiun twin). Closed intents diperluas dari 25 menjadi 26 (`penulis_ilmiah`). + +- **Eksepsi Pertumbuhan Katalog Resmi (Option B: 65 → 66)**: + - Katalog OpenCodeHighEnd di-unfreeze dari 65 menjadi 66 nama dengan menambahkan skill model-invoked `penulis-ilmiah` di `vendor/skill-allowlist.txt` dan `vendor/skill-policy.json`. + - Tidak ada twin yang dipensiunkan (*no twin retired*), serupa dengan preseden pertumbuhan terkelola pada rilis 0.1.7. + - Closed intents pada `templates/AGENTS.md`, `rules/00-routing.md`, dan `docs/routing.md` bertambah dari 25 menjadi 26 dengan pendaftaran intent `penulis_ilmiah` yang memetakan secara langsung ke skill `penulis-ilmiah`. +- **Skill `penulis-ilmiah` (`skills/penulis-ilmiah/`)**: + - Didedikasikan untuk penulisan dan penyuntingan draf naskah ilmiah Bahasa Indonesia berstandar EYD V dan kata baku KBBI untuk esai, bab skripsi, artikel jurnal nasional (SINTA), proposal lomba, dan abstrak Scopus dengan verifikasi rujukan mutlak (*fail-closed* / anti-halu sitasi). + - Dilengkapi basis pengetahuan operasional pada `references/`: kaidah EYD V lengkap (`eyd/`), pasangan kata baku KBBI (`kata_baku.md`), panduan struktur dan gaya penulisan ilmiah (`gaya_tulisan.md`), aturan format sitasi APA 7th & IEEE (`gaya_sitasi.md`), serta frasa klise AI yang dilarang (`frasa_terlarang.md`). +- **Server MCP First-Party `mcp/penulis-ilmiah/`**: + - Paket TypeScript mandiri (Node.js 20+) berbasis `@modelcontextprotocol/sdk` dan transport lokal stdio (dilarang menggunakan flag jaringan `--http`, `0.0.0.0`, atau docker). + - Menyediakan 5 tools terintegrasi: `cek_ejaan` (heuristik penulisan kata depan menempel, imbuhan pasif dipisah, spasi tanda baca, elipsis, integrasi opsional Hunspell `id_ID`), `cek_baku` (validasi kata baku KBBI per-kata dengan cache lokal 30 hari `~/.cache/penulis-ilmiah/kbbi-cache.json` dan rate limit online default 20/menit), `cari_rujukan` (pencarian publikasi ke Crossref, OpenAlex, Semantic Scholar), `verifikasi_rujukan` (validasi DOI Crossref, skor kemiripan judul, pengarang, tahun, dan keaktifan URL dengan status `VALID`, `TIDAK_COCOK`, `TIDAK_DITEMUKAN`), serta `format_sitasi` (pemformatan APA 7th dan IEEE dengan proteksi `wajib_terverifikasi` yang menolak memformat rujukan non-`VALID`). +- **Integrasi CLI & Diagnostik OCH**: + - Perintah CLI `opencode-he penulis-ilmiah {enable,disable}` untuk mengaktifkan/menonaktifkan MCP server `penulis-ilmiah` secara bedah (*surgical config update*). + - Didaftarkan sebagai optional MCP pada `vendor/sources.json` dan status `FOREIGN_ON_DEMAND` pada `vendor/mcp-wanted.json` (dengan transport policy stdio pada `vendor/mcp-policy.json`). + - Diagnostik `opencode-he doctor` mendeteksi konfigurasi valid sebagai `CONFIGURED`, status belum aktif sebagai `OPTIONAL_ABSENT`, dan menolak spesifikasi berbahaya (seperti `0.0.0.0` atau `--http`) sebagai `FAIL`. +- **Versi Produk**: Bump versi ke `0.1.18`. + ## 0.1.17 — 2026-10-04 Rilis sinkronisasi Wave 0.1.17 untuk pembaruan penuh aset anti-slop ke commit HEAD upstream `dmmulroy/anti-slop@c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b` (label versi `0.1.2+c44ef22 (untagged main, post-v0.1.2)`). Katalog tetap 65 (50 model + 15 manual). Closed intents tetap 25. Tidak ada penambahan atau pensiun skill (`vendor/skill-allowlist.txt` dan `vendor/skill-policy.json` tidak berubah). diff --git a/README.md b/README.md index 92cf44f..1145b97 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # OpenCodeHighEnd -OpenCode 2 overlay: 65 frozen routed skills, thin `AGENTS.md`, `opencode-he`. +OpenCode 2 overlay: 66 frozen routed skills, thin `AGENTS.md`, `opencode-he`. Installer and runtime overlay for [OpenCode 2](https://opencode.ai/v2/docs/). -Version **0.1.17**. The 65-skill catalog is strictly frozen. +Version **0.1.18**. The 66-skill catalog is strictly frozen. ## What it is -- 65 skills: 50 model-invoked, 15 manual slash commands (frozen; see [docs/CATALOG-FREEZE.md](docs/CATALOG-FREEZE.md)) -- A thin `AGENTS.md` router (lazy, one primary specialist, 25 closed intents) +- 66 skills: 51 model-invoked, 15 manual slash commands (frozen; see [docs/CATALOG-FREEZE.md](docs/CATALOG-FREEZE.md)) +- A thin `AGENTS.md` router (lazy, one primary specialist, 26 closed intents) - Core MCP: Codebase Memory (dual CLI and MCP graph adapters), Context7, shadcn - 12 Universal Design Banks + Operator Banks (34,700+ items across Identity, Motion, Section, Atomic, and Oversight) with zero-token local search & Google Drive v3 bootstrap - Design Bank path resolution via `~/.config/opencode/highend/config/design-bank.json` @@ -38,6 +38,16 @@ Version **0.1.17**. The 65-skill catalog is strictly frozen. - Not OpenCode 1.x (installer fails closed on 1.x) - Not claimed as macOS/Windows-tested (Linux x86_64 only for this release) +## What's new in 0.1.18 + +- **Official Catalog Unfreeze Exception (Option B: 65 → 66)**: Managed catalog growth from 65 to 66 skills (51 model-invoked + 15 manual slash commands) with no twin retired (`vendor/skill-allowlist.txt`, `docs/CATALOG-FREEZE.md`). +- **Closed Intent Expansion (25 → 26)**: Added `penulis_ilmiah` closed intent routing directly to `penulis-ilmiah` in `templates/AGENTS.md` (budget 72 lines ≤ 120), `rules/00-routing.md`, and `docs/routing.md`. +- **Skill `penulis-ilmiah` (`skills/penulis-ilmiah/`)**: Indonesian academic writing specialist for skripsi, SINTA journals, essays, and proposals with comprehensive knowledge references for EYD V (`references/eyd/`), KBBI standard word pairs (`references/kata_baku.md`), and forbidden AI clichés (`references/frasa_terlarang.md`). +- **First-Party TypeScript stdio MCP (`mcp/penulis-ilmiah/`)**: Standalone stdio MCP server (Node ≥20) providing 5 tools: `cek_ejaan`, `cek_baku`, `cari_rujukan`, `verifikasi_rujukan`, and `format_sitasi`. Strictly non-networked local stdio transport (no `--http`, no `0.0.0.0`). +- **Fail-Closed Citation Integrity**: `format_sitasi` enforces `wajib_terverifikasi: true` by rejecting unverified references with error code `REF_NOT_VERIFIED`. +- **CLI & Doctor Integration**: `opencode-he penulis-ilmiah {enable,disable}` provides parse-aware surgical config updates; `doctor` validates safe stdio configuration (`CONFIGURED`), reports `OPTIONAL_ABSENT` when not enabled, and flags invalid/network bindings as `FAIL`. +- **Test Coverage**: 410 unit/integration tests passing cleanly without pre-compiled artifacts, plus clean idempotency and install verification. + ## What's new in 0.1.13 - **Upstream Pins & MCP Modernization**: Core MCP `shadcn` bumped to `4.21.1`; `@reticlehq/server` pinned to `3.5.0`; `markitdown-mcp` pinned to `0.0.1a7` with `markitdown[all]==0.1.8`; `crawl4ai` standardized to `/mcp/sse` endpoint; `context7-mcp` shadow detection added to doctor. @@ -158,7 +168,7 @@ Default: repository evidence first. Then at most one specialist. | TS Oxlint install | `install-anti-slop` (explicit only) | | Architecture bake-off | `/architect` (manual) | -Warehouse: `api-design`, `contract-first`, `automation-audit-ops`, `code-tour`, `click-path-audit` (plus Wave 2 diagnostics). Wave 0.1.7 additions: `json-render` (generative UI from typed catalogs), `deck-design` (consulting-grade PPTX & 16:9 HTML slide decks), `pageindex` (vectorless tree-reasoning long document navigation). Wave 0.1.8 additions: `business-motion-film` (commercial launch films; retires `img2threejs`), `ninerouter` (multi-provider gateway stub; retires `prompt-optimizer`). Wave 0.1.9 body-only: merges `kaventro/motion-designer` into `business-motion-film` (product-film doctrine for real app UI; catalog strictly 65/65). Wave 0.1.10: `found-this-design` indexes Oversight Supply and the Design Bank bootstrap pin moves to DesignBank v3; pstack selected-skill provenance moves to `23e4138` (0.15.6) with host adaptation (`poteto-mode` rejected). Wave 0.1.11 body-only: adds `web_research` closed intent to `research` with `references/web-data.md`; Scrapling optional MCP; Agent-Reach pointer-only host tool; catalog strictly 65/65. Wave 0.1.12 body-only: `hyperframes` v0.8.119 refresh (declarative data attributes, CLI render); `awesome-opus5-5-videos` POINTER_ONLY prompt patterns; Matt cluster `GLOSSARY.md` migration; `humanizer` v3.1.0; catalog strictly 65/65. +Warehouse: `api-design`, `contract-first`, `automation-audit-ops`, `code-tour`, `click-path-audit` (plus Wave 2 diagnostics). Wave 0.1.7 additions: `json-render` (generative UI from typed catalogs), `deck-design` (consulting-grade PPTX & 16:9 HTML slide decks), `pageindex` (vectorless tree-reasoning long document navigation). Wave 0.1.8 additions: `business-motion-film` (commercial launch films; retires `img2threejs`), `ninerouter` (multi-provider gateway stub; retires `prompt-optimizer`). Wave 0.1.9 body-only: merges `kaventro/motion-designer` into `business-motion-film` (product-film doctrine for real app UI; catalog strictly 65/65). Wave 0.1.10: `found-this-design` indexes Oversight Supply and the Design Bank bootstrap pin moves to DesignBank v3; pstack selected-skill provenance moves to `23e4138` (0.15.6) with host adaptation (`poteto-mode` rejected). Wave 0.1.11 body-only: adds `web_research` closed intent to `research` with `references/web-data.md`; Scrapling optional MCP; Agent-Reach pointer-only host tool; catalog strictly 65/65. Wave 0.1.12 body-only: `hyperframes` v0.8.119 refresh (declarative data attributes, CLI render); `awesome-opus5-5-videos` POINTER_ONLY prompt patterns; Matt cluster `GLOSSARY.md` migration; `humanizer` v3.1.0; catalog strictly 65/65. Wave 0.1.18 unfreeze exception: Option B explicit catalog growth from 65 to 66 names (51 model-invoked + 15 manual) and 26 closed intents (`penulis_ilmiah`), adding `penulis-ilmiah` and first-party stdio MCP `mcp/penulis-ilmiah`. Examples: interactive product story told by scroll → `scroll-craft`. Unbroken camera through a miniature factory → `scroll-world`. Clean security dashboard → `impeccable`. Video, image generation, and Design V2 stay optional. diff --git a/VERSION b/VERSION index 04c5555..f8bc4c6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.17 +0.1.18 diff --git a/docs/CATALOG-FREEZE.md b/docs/CATALOG-FREEZE.md index 2e3c505..295e437 100644 --- a/docs/CATALOG-FREEZE.md +++ b/docs/CATALOG-FREEZE.md @@ -1,9 +1,10 @@ # Catalog Freeze Contract -This contract defines the immutable boundary and governance for the OpenCodeHighEnd catalog. The 65-skill catalog is strictly frozen. +This contract defines the immutable boundary and governance for the OpenCodeHighEnd catalog. The 66-skill catalog is strictly frozen. -- **Product version**: 0.1.17 -- **Catalog**: 65 names. 50 model-invoked under `skills/`. 15 manual under `manual-skills/` + `commands/`. +- **Product version**: 0.1.18 +- **Catalog**: 66 names. 51 model-invoked under `skills/`. 15 manual under `manual-skills/` + `commands/`. +- **Wave 0.1.18 unfreeze exception** (CHANGELOG 0.1.18): Option B explicit catalog growth from 65 to 66 names (51 model-invoked + 15 manual) to add first-party model-invoked skill `penulis-ilmiah` for Bahasa Indonesia academic drafting (EYD V, KBBI standard words, and fail-closed citation verification). No twin retired. Closed intent expanded from 25 to 26 (`penulis_ilmiah`). Accompanied by first-party stdio MCP server `mcp/penulis-ilmiah` added as `FOREIGN_ON_DEMAND` via `opencode-he penulis-ilmiah enable`. (Spent). - **Wave 0.1.17 body-only**: catalog strictly frozen at 65 (50 model + 15 manual). Zero catalog growth. No new exceptions. Upstream anti-slop full synchronization to `dmmulroy/anti-slop@c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b` (version `0.1.2+c44ef22`): vendored 38 assets byte-identical to upstream tree (18 generic rules, 5 Effect rules, shared helpers, and upstream-adapted eslint-stylistic padding rules under MIT); reconciled profiles (recommended stays 4 high-signal assertions; strict expanded to 18 generic rules plus companion native `oxc/no-accumulating-spread`; effect expanded to 5 rules); converted `manage.mjs update` to non-destructive dry-run review per upstream `references/update.md` merge doctrine (requiring `--force` to mutate live files) with per-name `jsPlugins` merging; restored `scripts/install.mjs` byte-identical to upstream (`db1f155b`); added `vendor/licenses/ESLINT-STYLISTIC-MIT.txt` and comprehensive notices. - **Wave 0.1.16 body-only**: catalog strictly frozen at 65 (50 model + 15 manual). Zero catalog growth. No new exceptions. Post-release correction and Playwright emulation refinement: restored c44ef22 upstream deferral record in docs/source-wave.md and corrected attribution of anti-slop asset synchronization (restoring 0.1.0 local drift to upstream e8c4880, not fixing upstream); documented mandatory permissions array for Playwright QA geolocation emulation and clarified runtime split (cross-browser grantPermissions/setGeolocation vs Chromium CDP timezone/locale overrides). - **Wave 0.1.15 body-only**: catalog strictly frozen at 65 (50 model + 15 manual). Zero catalog growth. No new exceptions. Upstream sync and hygiene: synchronized vendored Oxlint ruleset assets in `skills/install-anti-slop` from `dmmulroy/anti-slop@e8c4880471b23ab7f216fba7b27d173a6ef07d4c` (v0.1.2) restoring upstream no-widen-then-assert.ts (local drift since 0.1.0); standardized Playwright QA timezone/locale/geolocation emulation doctrine with `.playwright/cli.config.json` context options as primary cross-browser method and `run-code` as Chromium CDP runtime alternative; removed dead imports `repo_root` from `lib/design_v2/bootstrap.py` and `tarfile` from `tests/test_design_bootstrap.py`. diff --git a/docs/mcp.md b/docs/mcp.md index f3bdbcf..6d00563 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -21,6 +21,7 @@ Optional: - `markitdown` — `opencode-he markitdown enable` (local stdio via `uvx --from markitdown-mcp==0.0.1a7 --with markitdown[all]==0.1.8 markitdown-mcp`; Markdown ingest only) - `crawl4ai` — `opencode-he crawl4ai enable` (remote HTTP `http://127.0.0.1:11235/mcp/sse`; cloud via `--cloud` with `{env:CRAWL4AI_KEY}`) - `scrapling` — `opencode-he scrapling enable` (local stdio via `uvx --from scrapling[ai]==0.4.15 scrapling mcp`; structured web scraping) +- `penulis-ilmiah` — `opencode-he penulis-ilmiah enable` (local stdio via `npx tsx mcp/penulis-ilmiah/src/index.ts`; Indonesian academic writing, EYD V, KBBI standard words, and verified citations) - `exa` — foreign; never add/remove/overwrite Merge is parse-aware. Comment-free JSON is rewritten with `json.dumps`. JSONC with comments is patched surgically (owned MCP keys only). If surgical merge cannot be verified, install fails closed instead of destroying comments. @@ -41,6 +42,8 @@ Doctor reports `CONFIGURED` for owned MCP entries present in config. That is not `opencode-he scrapling enable` configures Scrapling as an optional local stdio MCP (`uvx --from scrapling[ai]==0.4.15 scrapling mcp`). It is `FOREIGN_ON_DEMAND` for structured web scraping and element extraction, not exploratory browser QA (which remains `playwright-qa`). Local stdio only: never `--http`, never bind `0.0.0.0`, never docker bind-all. Do not run `scrapling install` as it invokes `playwright install-deps` with sudo. The scraper is not vendored into `lib/`. `opencode-he scrapling disable` surgically removes only the scrapling server key. Absent is not a doctor failure; a malformed entry (including `--http` / `0.0.0.0`) fails closed. +`opencode-he penulis-ilmiah enable` configures the first-party `penulis-ilmiah` local stdio MCP (`npx tsx mcp/penulis-ilmiah/src/index.ts`). It is `FOREIGN_ON_DEMAND` for Indonesian academic drafting, EYD V spelling checks, KBBI standard words validation, and fail-closed citation verification against Crossref, OpenAlex, and Semantic Scholar. Local stdio only: `--http`, `0.0.0.0`, and docker bind-all are strictly forbidden. Polite Crossref access is configured via `{env:CROSSREF_MAILTO}`. `opencode-he penulis-ilmiah disable` surgically removes only the penulis-ilmiah server key. Absent is not a doctor failure; a malformed entry fails closed. + ## Evaluated, Skipped & Rejected - **Scrapling (`D4Vinci/Scrapling`)** — Promoted to `FOREIGN_ON_DEMAND` optional local stdio MCP in Wave 0.1.11 via `opencode-he scrapling enable` (pinned `0.4.15`). Not a core MCP; not vendored. Stdio only; `--http` and `0.0.0.0` forbidden. Never run `scrapling install`. diff --git a/docs/routing.md b/docs/routing.md index c1d3e76..53782b1 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -19,7 +19,7 @@ Every user request is classified into exactly one closed intent: ```text repo_understand | bug | security | perf | ui_direction | ui_implement | generative_ui motion | scroll_2d | scroll_3d | launch_film | gateway_llm | docs | ingest_md | prose -academic | longdoc_nav | web_research | browser_qa | architecture | warehouse | ops_data | video_html | demo_id | slides_pptx +academic | longdoc_nav | web_research | browser_qa | architecture | warehouse | ops_data | video_html | demo_id | slides_pptx | penulis_ilmiah ``` | Intent | Primary Route | Handoff Boundary / Rule | @@ -47,6 +47,7 @@ academic | longdoc_nav | web_research | browser_qa | architecture | warehouse | | `browser_qa` | `playwright-qa` → `browser-act` → `chrome-devtools-axi` → `click-path-audit` | 4-door hierarchy; isolated verification sessions | | `video_html` | `hyperframes` | Programmatic HTML-to-MP4 via headless Chrome + FFmpeg | | `demo_id` | `id-demo-video` (`/demo-video`) | Indonesian narrated app tour; cards via hyperframes | +| `penulis_ilmiah` | `penulis-ilmiah` | Indonesian academic drafting, EYD V, KBBI standard words, fail-closed citation verification | | `warehouse` | Only when user names the job | `agent-architecture-audit`, `cost-aware-llm-pipeline`, etc. | | `ops_data` | `supabase-ops` / `mongodb-ops` / `vercel-ops` | Operational data/hosting config; never generates UI | @@ -87,7 +88,7 @@ Browser verification follows four explicit doors: exploratory application UI rou Documents (answer, create, transform, extract, review, PDF/DOCX) route to `smartdoc`. Consulting PPTX / 16:9 slide decks route to `deck-design`. Long structured document tree/reasoning nav routes to `pageindex`. File-to-Markdown ingest routes to `markitdown`. Reusable book/module knowledge routes to `smartbook-ingest`. `/docx` and `/pdf` are missing aliases; nearest is `smartdoc`. `/pptx` routes to `deck-design`. Do not add `commands/pdf.md` or `commands/docx.md`. Impeccable `document` remains DESIGN.md generation. -Prose AI-tell removal and natural tone polishing route to `humanizer` (`/unslop` is its manual alias). Scholarly research, academic manuscripts, and structured peer critique route to `academic`. Web and social data gathering routes to `research` (`references/web-data.md`; read-only, not QA). Deterministic HTML composition rendered to video routes to `hyperframes`. Demo video aplikasi, walkthrough layar, narasi Indonesia, dan demo lomba route to `id-demo-video` (bukan `hyperframes` untuk durasi panjang utuh, bukan `playwright-qa`, bukan `visual-studio`). Kartu judul HTML→MP4 tetap `hyperframes`. Editorial technical diagrams (HTML/SVG) route to `diagram-design`. +Prose AI-tell removal and natural tone polishing route to `humanizer` (`/unslop` is its manual alias). Scholarly research, academic manuscripts, and structured peer critique route to `academic`. Penulisan dan penyuntingan naskah ilmiah Bahasa Indonesia (EYD V, kata baku KBBI, anti-halu sitasi) route to `penulis-ilmiah`. Web and social data gathering routes to `research` (`references/web-data.md`; read-only, not QA). Deterministic HTML composition rendered to video routes to `hyperframes`. Demo video aplikasi, walkthrough layar, narasi Indonesia, dan demo lomba route to `id-demo-video` (bukan `hyperframes` untuk durasi panjang utuh, bukan `playwright-qa`, bukan `visual-studio`). Kartu judul HTML→MP4 tetap `hyperframes`. Editorial technical diagrams (HTML/SVG) route to `diagram-design`. Warehouse diagnostics load only when the user names the job: `agent-architecture-audit` (architecture layers), `cost-aware-llm-pipeline` (token budgeting), `eval-harness` (benchmarks), and `skill-stocktake` (catalog hygiene; `prompt-optimizer` retired in 0.1.8 with prompt critique splitting to `humanizer` / `research` / `writing-for-agents`). Gateway 9Router routing (chat, image, video, TTS, STT, embeddings, web search) routes to `ninerouter` via `NINEROUTER_URL` (not an extra core MCP). Wave 3 warehouse procedures route to `api-design` (REST resources), `contract-first` (consumer/provider contracts), `automation-audit-ops` (live inventory), and `code-tour` (guided tours). Foreign harnesses (such as ECC control plane) remain `FOREIGN_ON_DEMAND`; never vendored, auto-merged, or shadowed. diff --git a/docs/security.md b/docs/security.md index b219fd7..2d1551e 100644 --- a/docs/security.md +++ b/docs/security.md @@ -17,6 +17,7 @@ - Design V2 doctor checks catalog JSONL and SQLite hashes against the canonical lock. - Model/provider names are opaque; do not print tokens or gateway maps - Scrapling is local stdio only (`uvx`); `--http` mode, `0.0.0.0` host binding, and Docker bind-all are strictly rejected. Never run `scrapling install` as it invokes `playwright install-deps` with sudo. Web scraping is read-only; never extract or store user desktop browser cookies or sessions (`--from-browser` is banned). +- Penulis Ilmiah is local stdio only (`npx tsx`); `--http` mode, `0.0.0.0` host binding, and Docker bind-all are strictly rejected. Citation verification is fail-closed; never invent DOIs, authors, or statistical claims. KBBI lookups are strictly on-demand per word with local caching; bulk dictionary scraping is banned. Secrets are never stored in git; environment variables (`CROSSREF_MAILTO`, `OPENALEX_API_KEY`, `S2_API_KEY`) are passed via `{env:...}`. - Agent-Reach is pointer-only; running `agent-reach install --system` is prohibited. Unmanaged skill directories in `~/.config/opencode/skills/` without `.opencode-highend.json` trigger `FOREIGN_SKILL_SHADOW` warnings in `doctor`. - `vendor/license-audit.json` lists every skill license **as evidenced**. Snapshot skills inherit GrokBestFriend MIT (`vendor/licenses/GROKBESTFRIEND-MIT.txt`). Design-bank media remains not-cleared. - GitHub rulesets: `main` and `v*` tags cannot be force-pushed or deleted. Signed commits/tags are `DEFERRED` until a maintainer signing key exists. GitHub release immutability is `NOT_CONFIGURED`. Integrity baseline is tag protection plus SHA256SUMS, SPDX SBOM, and `release-provenance.json`. diff --git a/docs/source-wave.md b/docs/source-wave.md index 43fddcb..ac5c2ea 100644 --- a/docs/source-wave.md +++ b/docs/source-wave.md @@ -45,6 +45,7 @@ Recorded per Phase 0 contract. | [semgrep / gitleaks / osv-scanner](https://github.com) | `semgrep` 1.179.0, `gitleaks` 8.30.1, `osv-scanner` 2.6.0 | Host security scanners. | **PIN_ONLY** | `skills/full-audit-keamanan` | Host scanner version pins recorded in `vendor/sources.json`. | | [unclecode/crawl4ai](https://github.com/unclecode/crawl4ai) | upstream ref | LLM-friendly web crawler & scraper MCP. Apache-2.0. | **FOREIGN_ON_DEMAND** | `docs/mcp.md` | Optional remote MCP (`http://127.0.0.1:11235/mcp/sse`, cloud via `--cloud`). Not vendored. Content extraction only, not exploratory QA eyes (`playwright-qa`). Legacy `/mcp` emits WARN CRAWL4AI_LEGACY_URL. Bind strictly 127.0.0.1, never 0.0.0.0. | | [D4Vinci/Scrapling](https://github.com/D4Vinci/Scrapling) | `333fa22b7a5821194ce66b59b11f4b16a6484f02` (v0.4.15) | Fast, undetectable web scraping library with adaptive selectors. BSD-3-Clause. | **FOREIGN_ON_DEMAND** | `vendor/sources.json`, `docs/mcp.md`, `skills/research/references/web-data.md` | Promoted to optional local stdio MCP (`uvx --from scrapling[ai]==0.4.15 scrapling mcp`). Not vendored. No `--http` or `0.0.0.0`. Never run `scrapling install`. | +| [penulis-ilmiah-kit](https://github.com/kuker24/OpenCodeHighEnd) | first-party export (2026-10-05) | Bahasa Indonesia academic writing, EYD V, KBBI standard words, and fail-closed citation verification. MIT. | **ADD** | `skills/penulis-ilmiah`, `mcp/penulis-ilmiah` | Option B unfreeze 65→66. First-party stdio MCP in TypeScript. References adapted from kit knowledge. Closed intent `penulis_ilmiah` (25→26). FOREIGN_ON_DEMAND via `opencode-he penulis-ilmiah enable`. | | [whaleyxbt/patchright-enhanced](https://github.com/whaleyxbt/patchright-enhanced) | upstream ref | Unofficial patched browser automation fork. | **REJECT** | None | Strictly rejected. Unofficial fork; carries security, maintenance, and divergence risks. | | [latent-spaces/brag](https://github.com/latent-spaces/brag) | `0.4.0` / upstream ref | 18-20s product launch video card recipe using HyperFrames. MIT. | **DONE** | `skills/hyperframes/references/brag.md` | Synthesized in 0.1.5 into `references/brag.md` (4-beat narrative contract, 60fps, 1080p, seekable frame timeline). Zero catalog bloat. | | [genspark-ai/genoffice](https://github.com/genspark-ai/genoffice) | upstream ref | AI-native desktop office suite (Docs, Sheets, Slides, PDF, Markdown). Apache-2.0. | **REJECT** | None | Multi-app Electron desktop suite requiring Genspark accounts/local models. Desktop app, not an agent coding skill overlay. | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 1c6af08..c2342db 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -32,6 +32,8 @@ Doctor `OPTIONAL_ABSENT` is not a core failure. `DEGRADED` is non-fatal unless ` `FAIL mcp:scrapling` — Scrapling MCP must use `type: local`, command starting with `uvx`, pinned package `scrapling[ai]==`, and end with `scrapling mcp`. Any use of `--http`, `0.0.0.0`, docker, or unpinned package fails closed. Reconfigure with `opencode-he scrapling enable`. +`FAIL mcp:penulis-ilmiah` — Penulis Ilmiah MCP must use `type: local` with stdio transport pointing to the first-party TypeScript entry (`mcp/penulis-ilmiah/src/index.ts`). Any use of `--http`, `0.0.0.0`, docker, or foreign paths fails closed. Reconfigure with `opencode-he penulis-ilmiah enable`. + `WARN FOREIGN_SKILL_SHADOW` — A foreign directory (such as `agent-reach` or `scrapling-official`) was detected in `~/.config/opencode/skills/` without an `.opencode-highend.json` ownership marker. These foreign skills hijack router intents and violate the frozen catalog. Delete the unmanaged skill directory manually. `doctor --deep` exit 1 with `NOT_CHECKED` — `opencode mcp list` failed or was empty; core MCP is not proven live. diff --git a/docs/wave-penulis-ilmiah.md b/docs/wave-penulis-ilmiah.md new file mode 100644 index 0000000..f3ee74e --- /dev/null +++ b/docs/wave-penulis-ilmiah.md @@ -0,0 +1,76 @@ +# Wave 0.1.18 — Penulis Ilmiah Design Stub + +Desain dan arsitektur integrasi modul `penulis-ilmiah` ke dalam OpenCodeHighEnd untuk Wave 0.1.18. + +## 1. Latar Belakang & Keputusan (Option B) + +Modul `penulis-ilmiah` dirancang untuk mendukung penulisan Bahasa Indonesia ilmiah yang benar sesuai dengan kaidah EYD V dan kata baku KBBI untuk esai, skripsi, jurnal SINTA, proposal lomba, dan abstrak jurnal internasional (Scopus) dengan **rujukan terverifikasi** (anti rujukan halu / anti-hallucination). + +### Keputusan Unfreeze 65 → 66 +Sesuai arahan pengembang (Option B): +- Batas katalog OpenCodeHighEnd di-unfreeze dari **65 menjadi 66 nama** (51 model-invoked di bawah `skills/` + 15 manual di bawah `manual-skills/` / `commands/`). +- Tidak ada *twin* yang dipensiunkan (*no twin retired*), serupa dengan *growth exception* terkelola pada Wave 0.1.7. +- Intent bertambah dari **25 menjadi 26 closed intents** (`penulis_ilmiah`). + +--- + +## 2. Batas Tanggung Jawab Antar-Spesialis (Boundaries) + +| Spesialis | Fokus & Tanggung Jawab | Batasan / Jangan Gunakan Untuk | +|---|---|---| +| **`penulis-ilmiah`** | Draf ilmiah Bahasa Indonesia (esai, skripsi, jurnal SINTA, proposal lomba), pemeriksaan kata depan/ejaan EYD V, validasi kata baku KBBI, dan verifikasi sitasi *fail-closed* via MCP. | Bukan untuk peer review metodologi bahasa Inggris murni, bukan pembersih gaya prosa umum, bukan render file. | +| **`academic`** | Tinjauan literatur komprehensif, telaah metodologi, manuskrip IMRaD English-first, telaah sejawat (*peer review*), dan matriks sanggahan revisi. | Bukan untuk pemeriksaan ejaan EYD V atau verifikasi kata baku KBBI. | +| **`humanizer` / `/unslop`** | Pembersihan ciri khas dan klise teks hasil luaran model AI (*AI tells*). | Bukan untuk logika verifikasi sitasi atau ejaan ilmiah formal. | +| **`smartdoc`** | Ingest dokumen (PDF/DOCX), ekstraksi tabel, OCR, dan rendering manuskrip terkunci ke format PDF/DOCX. | Kepemilikan klaim akademik dan konten ilmiah tetap di `penulis-ilmiah` atau `academic`. | +| **`research`** | Penelusuran dokumentasi perpustakaan/API pihak pertama dan data web/sosial. | Bukan untuk penulisan akademik berkaidah EYD. | + +--- + +## 3. Komponen & Arsitektur Sistem + +Modul terdiri dari dua komponen utama: + +1. **Skill Overlay (`skills/penulis-ilmiah/`)**: + - `SKILL.md`: Alur kerja draf → `cek_ejaan` & `cek_baku` → `verifikasi_rujukan` → `format_sitasi` → laporan terpisah (dokumen, daftar pustaka, laporan temuan). + - `references/`: Panduan operasional EYD V (`eyd/`), pasangan kata baku KBBI (`kata_baku.md`), panduan gaya tulisan (`gaya_tulisan.md`), gaya sitasi (`gaya_sitasi.md`), dan frasa terlarang AI (`frasa_terlarang.md`). + +2. **MCP Server First-Party (`mcp/penulis-ilmiah/`)**: + - Runtime: TypeScript (Node.js 20+), transport **stdio**. + - Dependensi ter-pin: `@modelcontextprotocol/sdk`, `zod`, `citation-js` (dengan plugin CSL APA/IEEE), `tsx`. + - **5 Tools Wajib**: + - `cek_ejaan`: Heuristik ejaan EYD V (kata depan *di/ke/dari* menempel vs imbuhan pasif, spasi tanda baca, elipsis, integrasi opsional Hunspell `id_ID`). + - `cek_baku`: Pemeriksaan kata baku terhadap kamus lokal (≥100 entri), cache lokal (`~/.cache/penulis-ilmiah/kbbi-cache.json` dengan TTL 30 hari), dan lookup KBBI eksternal dengan pembatasan frekuensi (rate limit, default 20/menit). + - `cari_rujukan`: Pencarian rujukan ke Crossref (polite pool via header `mailto`), OpenAlex, dan Semantic Scholar. + - `verifikasi_rujukan`: Verifikasi ketat DOI melalui Crossref, pencocokan skor kemiripan judul (ambang 0.85), pencocokan nama keluarga penulis, tahun terbit, dan verifikasi status aktif URL. Status: `VALID`, `TIDAK_COCOK`, `TIDAK_DITEMUKAN`. + - `format_sitasi`: Pemformatan sitasi APA 7 atau IEEE via `citation-js` dengan proteksi `wajib_terverifikasi` (menolak memformat rujukan yang belum `VALID`). + +--- + +## 4. Integrasi OCH (`FOREIGN_ON_DEMAND`) + +Mengikuti pola integrasi Wave 0.1.11 (`scrapling`): +- CLI OCH: `opencode-he penulis-ilmiah enable|disable`. +- Konfigurasi OpenCode 2 (`mcp.servers.penulis-ilmiah`): + - `type: "local"` + - `command: ["npx", "tsx", "mcp/penulis-ilmiah/src/index.ts"]` + - `disabled: false` + - `environment: {"CROSSREF_MAILTO": "{env:CROSSREF_MAILTO}"}` +- Diagnostik `opencode-he doctor`: + - Menampilkan `OPTIONAL_ABSENT mcp:penulis-ilmiah` saat belum diaktifkan (status hijau). + - Menampilkan `CONFIGURED mcp:penulis-ilmiah` saat aktif dengan konfigurasi valid. + - Menolak dan melaporkan `FAIL` apabila ditemukan binding jaringan publik (`0.0.0.0`), protokol `--http`, atau spesifikasi tak berizin. + +--- + +## 5. Kebijakan Keamanan & Integritas Ilmiah + +1. **Anti-Halu Rujukan (*Fail-Closed*)**: + - Dilarang keras mengarang DOI, judul, penulis, tahun, nomor halaman, atau data statistik. + - Rujukan yang berstatus `TIDAK_COCOK` atau `TIDAK_DITEMUKAN` tidak boleh dimasukkan ke dalam daftar pustaka akhir. +2. **Tidak Menjanjikan Evasion Plagiarisme**: + - Modul ini bukan untuk menghindari deteksi Turnitin atau pemalsuan skor similaritas. +3. **Pemberian Atribusi & Hak Cipta**: + - Tidak melakukan dump kamus KBBI secara massal ke dalam repositori. Hanya lookup per-kata sesuai kebutuhan pengguna. +4. **Privasi & Rahasia**: + - Tidak ada token API atau kunci rahasia yang disimpan di repositori. + - Variabel lingkungan yang didukung: `CROSSREF_MAILTO`, `OPENALEX_API_KEY`, `S2_API_KEY`, `KBBI_RATE_PER_MIN`, `PENULIS_CACHE_DIR`. diff --git a/lib/cli.py b/lib/cli.py index 358f5e5..ee2b2de 100755 --- a/lib/cli.py +++ b/lib/cli.py @@ -30,6 +30,8 @@ cmd_markitdown_enable, cmd_reticle_disable, cmd_reticle_enable, + cmd_penulis_ilmiah_disable, + cmd_penulis_ilmiah_enable, cmd_scrapling_disable, cmd_scrapling_enable, cmd_serena_enable, @@ -130,6 +132,9 @@ def build_parser() -> argparse.ArgumentParser: sc = sub.add_parser("scrapling", help="optional Scrapling local stdio MCP") sc.add_argument("action", choices=["enable", "disable"]) + pi = sub.add_parser("penulis-ilmiah", help="optional Penulis Ilmiah local stdio MCP") + pi.add_argument("action", choices=["enable", "disable"]) + sd = sub.add_parser("smartdoc", help="document profiles, extract, status") add_smartdoc_cli(sd) sb = sub.add_parser("smartbook", help="reusable SmartBook lifecycle") @@ -209,6 +214,10 @@ def main(argv: list[str] | None = None) -> int: if args.action == "enable": return cmd_scrapling_enable() return cmd_scrapling_disable() + if cmd == "penulis-ilmiah": + if args.action == "enable": + return cmd_penulis_ilmiah_enable() + return cmd_penulis_ilmiah_disable() if cmd == "smartdoc": return dispatch_smartdoc(args) if cmd == "smartbook": diff --git a/lib/doctor.py b/lib/doctor.py index ab0f915..6d3c130 100644 --- a/lib/doctor.py +++ b/lib/doctor.py @@ -120,14 +120,14 @@ def mcp_status_map() -> dict[str, str]: try: data = jsonc.load_path(cfg) except (OSError, json.JSONDecodeError, ValueError): - return {k: "FAIL" for k in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "markitdown", "crawl4ai", "scrapling", "exa")} + return {k: "FAIL" for k in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "markitdown", "crawl4ai", "scrapling", "penulis-ilmiah", "exa")} mcp = data.get("mcp") or {} if not isinstance(mcp, dict): - return {k: "FAIL" for k in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "markitdown", "crawl4ai", "scrapling", "exa")} + return {k: "FAIL" for k in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "markitdown", "crawl4ai", "scrapling", "penulis-ilmiah", "exa")} servers = jsonc.mcp_servers_from_config(data) owned = {"codebase-memory-mcp", "context7", "shadcn"} - optional = {"serena", "stitch", "reticle", "ui-skills", "markitdown", "crawl4ai", "scrapling", "exa"} - for name in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "markitdown", "crawl4ai", "scrapling", "exa"): + optional = {"serena", "stitch", "reticle", "ui-skills", "markitdown", "crawl4ai", "scrapling", "penulis-ilmiah", "exa"} + for name in ("codebase-memory-mcp", "context7", "shadcn", "serena", "stitch", "reticle", "ui-skills", "markitdown", "crawl4ai", "scrapling", "penulis-ilmiah", "exa"): spec = servers.get(name) if spec is None: out[name] = "OPTIONAL_ABSENT" if name in optional else "FAIL" @@ -274,6 +274,28 @@ def mcp_status_map() -> dict[str, str]: continue out[name] = "CONFIGURED" continue + if name == "penulis-ilmiah": + typ = spec.get("type") + cmd = spec.get("command") + if typ != "local" or not isinstance(cmd, list) or not cmd: + out[name] = "FAIL" + continue + joined = " ".join(str(part) for part in cmd) + forbidden = ( + "--http", + "--host", + "--port", + "0.0.0.0", + "docker", + ) + if any(bad in joined for bad in forbidden): + out[name] = "FAIL" + continue + if not any("penulis-ilmiah" in str(part) for part in cmd): + out[name] = "FAIL" + continue + out[name] = "CONFIGURED" + continue if name not in owned: out[name] = "FOREIGN" continue diff --git a/lib/install.py b/lib/install.py index 251f1ac..1067ad7 100644 --- a/lib/install.py +++ b/lib/install.py @@ -966,7 +966,7 @@ def take(src: Path, dest: Path) -> None: "modelInvokedSkills": meta["model"], "manualSkills": meta["manual"], "ownedMcp": list(OWNED_MCP), - "optionalMcp": ["serena", "stitch", "reticle", "ui-skills", "markitdown", "crawl4ai", "scrapling", "exa"], + "optionalMcp": ["serena", "stitch", "reticle", "ui-skills", "markitdown", "crawl4ai", "scrapling", "penulis-ilmiah", "exa"], "designBank": { "root": bank_root, "source": bank_source, @@ -1606,4 +1606,25 @@ def cmd_scrapling_disable() -> int: return _optional_mcp_disable("scrapling") +def cmd_penulis_ilmiah_enable() -> int: + mcp_entry = repo_root() / "mcp" / "penulis-ilmiah" / "src" / "index.ts" + spec: dict[str, object] = { + "type": "local", + "command": ["npx", "tsx", str(mcp_entry)], + "disabled": False, + "environment": { + "CROSSREF_MAILTO": "{env:CROSSREF_MAILTO}" + } + } + rc = _optional_mcp_enable("penulis-ilmiah", spec) + if not which("npx"): + info("npx not found on PATH; Node.js 20+ required to run penulis-ilmiah stdio MCP.") + return rc + + +def cmd_penulis_ilmiah_disable() -> int: + return _optional_mcp_disable("penulis-ilmiah") + + + diff --git a/mcp/penulis-ilmiah/.env.example b/mcp/penulis-ilmiah/.env.example new file mode 100644 index 0000000..39d82e1 --- /dev/null +++ b/mcp/penulis-ilmiah/.env.example @@ -0,0 +1,14 @@ +# Mailto header for polite pool Crossref (highly recommended) +CROSSREF_MAILTO=fahmiharun0812@gmail.com + +# Optional OpenAlex API key to increase rate limit +OPENALEX_API_KEY= + +# Optional Semantic Scholar API key +S2_API_KEY= + +# Maximum KBBI online lookup queries per minute (default: 20) +KBBI_RATE_PER_MIN=20 + +# Cache directory for KBBI and DOI metadata (default: ~/.cache/penulis-ilmiah) +PENULIS_CACHE_DIR= diff --git a/mcp/penulis-ilmiah/README.md b/mcp/penulis-ilmiah/README.md new file mode 100644 index 0000000..bae750e --- /dev/null +++ b/mcp/penulis-ilmiah/README.md @@ -0,0 +1,37 @@ +# Penulis Ilmiah MCP Server + +Model Context Protocol (MCP) server lokal (transport `stdio`) untuk memeriksa kaidah penulisan ilmiah Bahasa Indonesia (EYD V, kata baku KBBI) dan memverifikasi rujukan ilmiah ke basis data resmi (Crossref, OpenAlex, Semantic Scholar). + +## Fitur & Tools + +1. **`cek_ejaan`**: Memeriksa ejaan EYD V (kata depan *di/ke/dari* menempel vs imbuhan pasif dipisah, spasi sebelum tanda baca, format tanda elipsis, integrasi opsional Hunspell `id_ID`). +2. **`cek_baku`**: Memeriksa kebakuan kata terhadap daftar pasangan kata baku KBBI (≥100 entri), cache lokal (`~/.cache/penulis-ilmiah/kbbi-cache.json`), dan lookup terbatas per-kata. +3. **`cari_rujukan`**: Mencari publikasi ilmiah di Crossref dan OpenAlex dengan filter tahun penerbitan. +4. **`verifikasi_rujukan`**: Memverifikasi keabsahan DOI, pencocokan skor kemiripan judul (ambang batas default 0.85), nama penulis, tahun, dan keaktifan URL secara *fail-closed*. +5. **`format_sitasi`**: Memformat rujukan yang telah `VALID` ke gaya sitasi APA 7th atau IEEE menggunakan `citation-js`. Menolak memformat rujukan yang gagal verifikasi. + +## Menjalankan Server + +```bash +# Menjalankan langsung via tsx +npx tsx src/index.ts + +# Atau via build dist +npm run build +node dist/index.js +``` + +## Konfigurasi Lingkungan (Environment Variables) + +- `CROSSREF_MAILTO`: Alamat surel polite pool Crossref (disarankan: `fahmiharun0812@gmail.com`). +- `OPENALEX_API_KEY`: Kunci API opsional untuk OpenAlex. +- `S2_API_KEY`: Kunci API opsional untuk Semantic Scholar. +- `KBBI_RATE_PER_MIN`: Batas maksimal pencarian online KBBI per menit (default: 20). +- `PENULIS_CACHE_DIR`: Direktori penyimpanan cache (default: `~/.cache/penulis-ilmiah`). + +## Dependensi & Pinning + +Versi pustaka di-pin secara exact di `package.json` dan terkunci di `package-lock.json`: +- `@modelcontextprotocol/sdk`: `1.32.1` +- `citation-js`: `0.7.22` +- `zod`: `3.25.76` diff --git a/mcp/penulis-ilmiah/package-lock.json b/mcp/penulis-ilmiah/package-lock.json new file mode 100644 index 0000000..87e718f --- /dev/null +++ b/mcp/penulis-ilmiah/package-lock.json @@ -0,0 +1,2555 @@ +{ + "name": "penulis-ilmiah-mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "penulis-ilmiah-mcp", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "1.32.1", + "citation-js": "0.7.22", + "zod": "3.25.76" + }, + "bin": { + "penulis-ilmiah-mcp": "dist/index.js" + }, + "devDependencies": { + "@types/node": "22.20.5", + "tsx": "4.23.15", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@citation-js/cli": { + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/@citation-js/cli/-/cli-0.7.21.tgz", + "integrity": "sha512-9OS49kC7Z8MKar7Wf6rMOuPyU5eg3mYU4q7iQD+ss9G6bEfTWolUFfMVqZ5P0EvgmDoM5He1ENygRdk6is7Btw==", + "license": "MIT", + "dependencies": { + "@citation-js/core": "^0.7.21", + "@citation-js/plugin-bibjson": "^0.7.21", + "@citation-js/plugin-bibtex": "^0.7.21", + "@citation-js/plugin-csl": "^0.7.21", + "@citation-js/plugin-doi": "^0.7.21", + "@citation-js/plugin-ris": "^0.7.21", + "@citation-js/plugin-wikidata": "^0.7.21", + "commander": "^11.0.0" + }, + "bin": { + "citation-js": "lib/index.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@citation-js/core": { + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.21.tgz", + "integrity": "sha512-Vobv2/Yfnn6C6BVO/pvj7madQ7Mfzl83/jAWwixbemGF6ZThhGMz8++FD9hWHyHXDMYuLGa6fK68c2VsolZmTA==", + "license": "MIT", + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2", + "fetch-ponyfill": "^7.1.0", + "sync-fetch": "^0.4.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@citation-js/date": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@citation-js/date/-/date-0.5.1.tgz", + "integrity": "sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@citation-js/name": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@citation-js/name/-/name-0.4.2.tgz", + "integrity": "sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@citation-js/plugin-bibjson": { + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibjson/-/plugin-bibjson-0.7.21.tgz", + "integrity": "sha512-Q3eRpeV3pDqX91fvJ8xoYGJk4jB5MQD2woEr6GAoAauAzGVE19J5Lw0SksCHegoAn4z0ZbWMXtOq6rwZPtERbA==", + "license": "MIT", + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@citation-js/plugin-bibtex": { + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.21.tgz", + "integrity": "sha512-O008pSsJgiYKn4+7gAWrbNpNdUH++aMeYmZaJ2oFQ8X1tcY5jNBxJcr0zZojNtUi5CVOaXXHQ0yIifoUhuF2Vg==", + "license": "MIT", + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2", + "moo": "^0.5.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@citation-js/plugin-csl": { + "version": "0.7.22", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.22.tgz", + "integrity": "sha512-/rGdtbeP3nS4uZDdEbQUHT8PrUcIs0da2t+sWMKYXoOhXQqfw3oJJ7p4tUD+R8lptyIR5Eq20/DFk/kQDdLpYg==", + "license": "MIT", + "dependencies": { + "@citation-js/date": "^0.5.0", + "citeproc": "^2.4.6" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@citation-js/plugin-doi": { + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-doi/-/plugin-doi-0.7.21.tgz", + "integrity": "sha512-KmViSt6CjfQS5dPHlHI6m6ahfshHUivSd8jQ0z5SvZCj2mKMH+t85tY57/880Xj9hVoXdRLmh53Uf0b+craNHA==", + "license": "MIT", + "dependencies": { + "@citation-js/date": "^0.5.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@citation-js/plugin-ris": { + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-ris/-/plugin-ris-0.7.21.tgz", + "integrity": "sha512-swVpYTEYHgO8DRjCeWzaQLQa9B9BiO1bCH67sU+U/dbNw7S71wAHjK9MNI5CqRMndwKldOg6SswBQWzLtBsbeA==", + "license": "MIT", + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@citation-js/plugin-wikidata": { + "version": "0.7.22", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-wikidata/-/plugin-wikidata-0.7.22.tgz", + "integrity": "sha512-PkPOF7PLhLCwOJ0zq3W3BKw1PIW8rA+2ueLZE4Jlx7TZ03lDiri2D/h62curkY2dzWiY4a4HQKvJxS2uGj5Jxg==", + "license": "MIT", + "dependencies": { + "@citation-js/date": "^0.5.0", + "@citation-js/name": "^0.4.2", + "@larsgw/wikibase-sdk": "^10.2.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@citation-js/core": "^0.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.3.tgz", + "integrity": "sha512-TA//nWMqPhbfdfneACk6t5a9eqbS9lABEPyKn0/xZTah3H3U2XaVg85rJFl0/Fyit0I552YDHgXGVSf3GwqbUw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@larsgw/wikibase-sdk": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@larsgw/wikibase-sdk/-/wikibase-sdk-10.2.1.tgz", + "integrity": "sha512-tn5SsNJnM/zwe0sW+oKBZIJVlQSaq0hIWbZ3F0Zxnhh/AsgqZseP63xRGmwdgkvpI8NyRfjvdMQnzIavTMPglA==", + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.32.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz", + "integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "license": "BSD-2-Clause" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citation-js": { + "version": "0.7.22", + "resolved": "https://registry.npmjs.org/citation-js/-/citation-js-0.7.22.tgz", + "integrity": "sha512-2Vn3gukzZnjF2Y0KZ7B2wUAcGjsoRRQHQgnBgdMlUZpDYgNUPM2fo+4+xgS3rszq38OLdXFsCR6vmTdCWHkKZw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@citation-js/cli": "0.7.21", + "@citation-js/core": "0.7.21", + "@citation-js/date": "0.5.1", + "@citation-js/name": "0.4.2", + "@citation-js/plugin-bibjson": "0.7.21", + "@citation-js/plugin-bibtex": "0.7.21", + "@citation-js/plugin-csl": "0.7.22", + "@citation-js/plugin-doi": "0.7.21", + "@citation-js/plugin-ris": "0.7.21", + "@citation-js/plugin-wikidata": "0.7.22", + "citeproc": "^2.4.59", + "patch-package": "^8.0.1" + }, + "bin": { + "citation-js": "bin/cmd.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/citeproc": { + "version": "2.4.63", + "resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.63.tgz", + "integrity": "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q==", + "license": "CPAL-1.0 OR AGPL-1.0" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fetch-ponyfill": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz", + "integrity": "sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==", + "license": "MIT", + "dependencies": { + "node-fetch": "~2.6.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.13", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz", + "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/moo": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", + "integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==", + "license": "BSD-3-Clause" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sync-fetch": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz", + "integrity": "sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==", + "license": "MIT", + "dependencies": { + "buffer": "^5.7.1", + "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp/penulis-ilmiah/package.json b/mcp/penulis-ilmiah/package.json new file mode 100644 index 0000000..cd2308d --- /dev/null +++ b/mcp/penulis-ilmiah/package.json @@ -0,0 +1,30 @@ +{ + "name": "penulis-ilmiah-mcp", + "version": "0.1.0", + "description": "MCP server for Bahasa Indonesia academic writing, EYD V spelling, KBBI standard words, and verified citations", + "type": "module", + "main": "dist/index.js", + "bin": { + "penulis-ilmiah-mcp": "dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsx src/index.ts", + "start": "node dist/index.js", + "test": "node --test" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "1.32.1", + "citation-js": "0.7.22", + "zod": "3.25.76" + }, + "devDependencies": { + "@types/node": "22.20.5", + "tsx": "4.23.15", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "license": "MIT" +} diff --git a/mcp/penulis-ilmiah/src/index.ts b/mcp/penulis-ilmiah/src/index.ts new file mode 100644 index 0000000..a99f3f8 --- /dev/null +++ b/mcp/penulis-ilmiah/src/index.ts @@ -0,0 +1,184 @@ +#!/usr/bin/env node +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; + +import { cekEjaan } from './tools/cek_ejaan.js'; +import { cekBaku } from './tools/cek_baku.js'; +import { cariRujukan } from './tools/cari_rujukan.js'; +import { verifikasiRujukan } from './tools/verifikasi_rujukan.js'; +import { formatSitasi } from './tools/format_sitasi.js'; + +// Pastikan transport stdio saja (larang --http, 0.0.0.0) +const args = process.argv.slice(2); +if (args.includes('--http') || args.some(a => a.includes('0.0.0.0'))) { + console.error('ERROR: Server penulis-ilmiah hanya mendukung transport lokal stdio. Flag --http dan 0.0.0.0 dilarang.'); + process.exit(1); +} + +const server = new McpServer({ + name: 'penulis-ilmiah', + version: '0.1.0' +}); + +// Tool 1: cek_ejaan +server.tool( + 'cek_ejaan', + 'Memeriksa ejaan EYD V (kata depan di/ke/dari menempel, imbuhan pasif dipisah, spasi tanda baca, elipsis, integrasi Hunspell opsional)', + { + teks: z.string().min(1, 'Teks wajib diisi'), + pakai_hunspell: z.boolean().default(false).describe('Gunakan Hunspell id_ID bila tersedia di sistem host'), + bahasa: z.enum(['id']).default('id').describe('Bahasa teks (default: id)') + }, + async (params) => { + const result = await cekEjaan(params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + }; + } +); + +// Tool 2: cek_baku +server.tool( + 'cek_baku', + 'Memeriksa kata tidak baku terhadap kamus baku KBBI lokal, cache lokal, dan fallback KBBI online', + { + teks: z.string().min(1, 'Teks wajib diisi'), + fallback_kbbi: z.boolean().default(true).describe('Fallback online lookup per kata jika kata belum ada di daftar lokal'), + max_lookup: z.number().int().min(1).max(50).default(20).describe('Batas maksimal kata yang dicari ke KBBI online per panggilan') + }, + async (params) => { + const result = await cekBaku(params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + }; + } +); + +// Tool 3: cari_rujukan +server.tool( + 'cari_rujukan', + 'Mencari publikasi ilmiah ke Crossref, OpenAlex, atau Semantic Scholar (kandidat rujukan)', + { + query: z.string().min(1, 'Query pencarian rujukan'), + sumber: z.array(z.enum(['crossref', 'openalex', 'semanticscholar'])).default(['crossref', 'openalex']).describe('Basis data sumber pencarian'), + limit: z.number().int().min(1).max(20).default(5).describe('Jumlah hasil maksimal (1-20)'), + tahun_min: z.number().int().optional().describe('Filter tahun penerbitan minimal'), + tahun_max: z.number().int().optional().describe('Filter tahun penerbitan maksimal') + }, + async (params) => { + const result = await cariRujukan(params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + }; + } +); + +// Tool 4: verifikasi_rujukan +server.tool( + 'verifikasi_rujukan', + 'Memverifikasi keaslian DOI, keselarasan judul, pengarang, tahun, dan keaktifan URL secara sahih (anti-halu)', + { + doi: z.string().optional().describe('DOI artikel (contoh: 10.1038/nature14539)'), + judul: z.string().optional().describe('Judul publikasi yang diklaim'), + penulis: z.array(z.string()).optional().describe('Daftar nama penulis'), + tahun: z.number().int().optional().describe('Tahun penerbitan'), + url: z.string().optional().describe('URL publikasi atau DOI'), + ambang_judul: z.number().min(0.5).max(1.0).default(0.85).describe('Ambang batas skor kemiripan judul (0.5 - 1.0)') + }, + async (params) => { + if (!params.doi && !params.judul && !params.url) { + return { + isError: true, + content: [ + { + type: 'text', + text: JSON.stringify({ + error: { + code: 'INVALID_INPUT', + message: 'Minimal salah satu harus diberikan: doi, judul, atau url' + } + }, null, 2) + } + ] + }; + } + const result = await verifikasiRujukan(params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + }; + } +); + +// Tool 5: format_sitasi +server.tool( + 'format_sitasi', + 'Memformat metadata rujukan terverifikasi menjadi sitasi daftar pustaka dan sitasi dalam teks (APA 7th atau IEEE)', + { + gaya: z.enum(['apa7', 'ieee']).describe('Gaya sitasi: apa7 atau ieee'), + item: z.object({ + doi: z.string().optional(), + judul: z.string().optional(), + penulis: z.array(z.string()).optional(), + tahun: z.number().int().optional(), + venue: z.string().optional(), + volume: z.string().optional(), + issue: z.string().optional(), + halaman: z.string().optional(), + url: z.string().optional() + }).describe('Objek metadata artikel yang telah VALID'), + wajib_terverifikasi: z.boolean().default(true).describe('Tolak pemformatan jika rujukan belum berstatus VALID (fail-closed)') + }, + async (params) => { + const result = await formatSitasi(params); + if ('error' in result) { + return { + isError: true, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + }; + } + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + }; + } +); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((err) => { + console.error('Fatal MCP error:', err); + process.exit(1); +}); diff --git a/mcp/penulis-ilmiah/src/lib/crossref.ts b/mcp/penulis-ilmiah/src/lib/crossref.ts new file mode 100644 index 0000000..e0a8f2c --- /dev/null +++ b/mcp/penulis-ilmiah/src/lib/crossref.ts @@ -0,0 +1,107 @@ +import { RujukanItem } from './types.js'; + +function getMailto(): string { + return process.env.CROSSREF_MAILTO || 'open-code-highend@users.noreply.github.com'; +} + +function getUserAgent(): string { + return `OpenCodeHighEnd-PenulisIlmiah/0.1.18 (mailto:${getMailto()})`; +} + +export interface CrossrefWorkItem { + DOI?: string; + title?: string[]; + author?: Array<{ family?: string; given?: string }>; + issued?: { 'date-parts'?: number[][] }; + created?: { 'date-parts'?: number[][] }; + 'container-title'?: string[]; + volume?: string; + issue?: string; + page?: string; + resource?: { primary?: { URL?: string } }; + URL?: string; +} + +export async function fetchCrossrefWork(doi: string): Promise { + const cleanDoi = doi.replace(/^https?:\/\/doi\.org\//i, '').replace(/^doi:\s*/i, '').trim(); + const url = `https://api.crossref.org/works/${encodeURIComponent(cleanDoi)}`; + + try { + const res = await fetch(url, { + method: 'GET', + headers: { + 'User-Agent': getUserAgent(), + 'Accept': 'application/json' + }, + signal: AbortSignal.timeout(8000) + }); + + if (res.status === 404) { + return null; + } + + if (res.status === 429) { + // Tunggu sesaat jika rate limit + await new Promise(r => setTimeout(r, 1000)); + return null; + } + + if (!res.ok) { + return null; + } + + const data = await res.json() as { message?: CrossrefWorkItem }; + return data.message || null; + } catch { + return null; + } +} + +export async function searchCrossrefWorks( + query: string, + limit: number = 5, + tahunMin?: number, + tahunMax?: number +): Promise { + let url = `https://api.crossref.org/works?query=${encodeURIComponent(query)}&rows=${limit}`; + + const filters: string[] = []; + if (tahunMin) filters.push(`from-pub-date:${tahunMin}`); + if (tahunMax) filters.push(`until-pub-date:${tahunMax}`); + if (filters.length > 0) { + url += `&filter=${encodeURIComponent(filters.join(','))}`; + } + + try { + const res = await fetch(url, { + method: 'GET', + headers: { + 'User-Agent': getUserAgent(), + 'Accept': 'application/json' + }, + signal: AbortSignal.timeout(8000) + }); + + if (!res.ok) return []; + + const data = await res.json() as { message?: { items?: CrossrefWorkItem[] } }; + const items = data.message?.items || []; + + return items.map(item => { + const year = item.issued?.['date-parts']?.[0]?.[0] || item.created?.['date-parts']?.[0]?.[0] || null; + const authors = item.author?.map(a => `${a.family || ''}, ${a.given || ''}`.trim()).filter(Boolean) || []; + + return { + judul: item.title?.[0] || '', + penulis: authors, + tahun: year, + doi: item.DOI, + url: item.resource?.primary?.URL || item.URL || (item.DOI ? `https://doi.org/${item.DOI}` : undefined), + sumber: 'crossref', + venue: item['container-title']?.[0] + }; + }); + } catch { + return []; + } +} diff --git a/mcp/penulis-ilmiah/src/lib/dictionary.ts b/mcp/penulis-ilmiah/src/lib/dictionary.ts new file mode 100644 index 0000000..439d852 --- /dev/null +++ b/mcp/penulis-ilmiah/src/lib/dictionary.ts @@ -0,0 +1,125 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export interface KataBakuEntry { + tidak_baku: string; + baku: string; + url_kbbi: string; + catatan: string; + cek_manusia: boolean; +} + +let cachedDictionary: Map | null = null; +let cachedPhraseList: KataBakuEntry[] | null = null; + +/** + * Mencari berkas kata_baku.md di berbagai kemungkinan lokasi + */ +function findKataBakuFile(): string | null { + const candidates = [ + // Relatif dari src/lib + path.resolve(__dirname, '../../../../skills/penulis-ilmiah/references/kata_baku.md'), + // Relatif dari dist/lib + path.resolve(__dirname, '../../../skills/penulis-ilmiah/references/kata_baku.md'), + // Relatif dari current working dir + path.resolve(process.cwd(), 'skills/penulis-ilmiah/references/kata_baku.md'), + path.resolve(process.cwd(), 'references/kata_baku.md'), + // Lokasi di ~/.config/opencode + path.resolve(process.env.HOME || '', '.config/opencode/skills/penulis-ilmiah/references/kata_baku.md') + ]; + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; +} + +/** + * Memuat dan mengurai isi kata_baku.md + */ +export function loadKataBaku(): { map: Map; phrases: KataBakuEntry[] } { + if (cachedDictionary && cachedPhraseList) { + return { map: cachedDictionary, phrases: cachedPhraseList }; + } + + const dictMap = new Map(); + const phraseList: KataBakuEntry[] = []; + + const filePath = findKataBakuFile(); + if (filePath && fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n'); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed.startsWith('|') || trimmed.startsWith('| #') || trimmed.startsWith('|---')) { + continue; + } + + const cols = trimmed.split('|').map(c => c.trim()); + // format: | # | Tidak baku / diragukan | Baku / saran | URL cek | Catatan | cek_manusia | + if (cols.length >= 7) { + const tidakBaku = cols[2]; + const baku = cols[3]; + const url = cols[4]; + const catatan = cols[5]; + const cekManusia = cols[6] === 'ya'; + + if (tidakBaku && baku) { + const entry: KataBakuEntry = { + tidak_baku: tidakBaku, + baku: baku, + url_kbbi: url, + catatan: catatan, + cek_manusia: cekManusia + }; + + const key = tidakBaku.toLowerCase(); + dictMap.set(key, entry); + + if (key.includes(' ')) { + phraseList.push(entry); + } + } + } + } + } + + // Tambahkan fallback entri penting jika berkas tidak ditemukan + if (dictMap.size === 0) { + const fallbackList = [ + { tidak_baku: 'analisa', baku: 'analisis', url_kbbi: 'https://kbbi.web.id/analisis', catatan: 'umum ilmiah', cek_manusia: false }, + { tidak_baku: 'aktipitas', baku: 'aktivitas', url_kbbi: 'https://kbbi.web.id/aktivitas', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'aktifitas', baku: 'aktivitas', url_kbbi: 'https://kbbi.web.id/aktivitas', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'apotik', baku: 'apotek', url_kbbi: 'https://kbbi.web.id/apotek', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'ijin', baku: 'izin', url_kbbi: 'https://kbbi.web.id/izin', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'diijinkan', baku: 'diizinkan', url_kbbi: 'https://kbbi.web.id/diizinkan', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'obyek', baku: 'objek', url_kbbi: 'https://kbbi.web.id/objek', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'metoda', baku: 'metode', url_kbbi: 'https://kbbi.web.id/metode', catatan: 'umum ilmiah', cek_manusia: false }, + { tidak_baku: 'propinsi', baku: 'provinsi', url_kbbi: 'https://kbbi.web.id/provinsi', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'prosentase', baku: 'persentase', url_kbbi: 'https://kbbi.web.id/persentase', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'resiko', baku: 'risiko', url_kbbi: 'https://kbbi.web.id/risiko', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'silahkan', baku: 'silakan', url_kbbi: 'https://kbbi.web.id/silakan', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'merubah', baku: 'mengubah', url_kbbi: 'https://kbbi.web.id/mengubah', catatan: 'umum', cek_manusia: false }, + { tidak_baku: 'terimakasih', baku: 'terima kasih', url_kbbi: 'https://kbbi.web.id/terima', catatan: 'umum', cek_manusia: false } + ]; + + for (const fb of fallbackList) { + dictMap.set(fb.tidak_baku.toLowerCase(), fb); + } + } + + // Urutkan frasa berdasarkan panjang (descending) agar frasa panjang dicocokkan terlebih dahulu + phraseList.sort((a, b) => b.tidak_baku.length - a.tidak_baku.length); + + cachedDictionary = dictMap; + cachedPhraseList = phraseList; + + return { map: dictMap, phrases: phraseList }; +} diff --git a/mcp/penulis-ilmiah/src/lib/kbbi.ts b/mcp/penulis-ilmiah/src/lib/kbbi.ts new file mode 100644 index 0000000..12cd033 --- /dev/null +++ b/mcp/penulis-ilmiah/src/lib/kbbi.ts @@ -0,0 +1,179 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; + +export interface KbbiCacheItem { + kata: string; + status: 'tidak_baku' | 'baku' | 'tidak_diketahui' | 'perlu_cek_manusia'; + saran_baku: string; + url_kbbi: string; + timestamp: number; +} + +interface KbbiCacheStore { + [kata: string]: KbbiCacheItem; +} + +const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 hari + +function getCacheDir(): string { + if (process.env.PENULIS_CACHE_DIR) { + return process.env.PENULIS_CACHE_DIR; + } + return path.join(os.homedir(), '.cache', 'penulis-ilmiah'); +} + +function getCacheFilePath(): string { + return path.join(getCacheDir(), 'kbbi-cache.json'); +} + +let memoryCache: KbbiCacheStore | null = null; + +function loadCache(): KbbiCacheStore { + if (memoryCache) return memoryCache; + const filePath = getCacheFilePath(); + try { + if (fs.existsSync(filePath)) { + const data = fs.readFileSync(filePath, 'utf-8'); + memoryCache = JSON.parse(data); + return memoryCache!; + } + } catch { + // Abaikan jika berkas korup + } + memoryCache = {}; + return memoryCache; +} + +function saveCache(cache: KbbiCacheStore): void { + try { + const dir = getCacheDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(getCacheFilePath(), JSON.stringify(cache, null, 2), 'utf-8'); + } catch { + // Abaikan jika gagal menyimpan berkas cache + } +} + +export function getCachedWord(kata: string): KbbiCacheItem | null { + const cache = loadCache(); + const key = kata.toLowerCase().trim(); + const item = cache[key]; + if (!item) return null; + + if (Date.now() - item.timestamp > CACHE_TTL_MS) { + delete cache[key]; + saveCache(cache); + return null; + } + return item; +} + +export function setCachedWord(kata: string, data: Omit): void { + const cache = loadCache(); + const key = kata.toLowerCase().trim(); + cache[key] = { + ...data, + timestamp: Date.now() + }; + saveCache(cache); +} + +// Rate Limiter untuk online lookup +const rateLimitPerMin = parseInt(process.env.KBBI_RATE_PER_MIN || '20', 10); +const minIntervalMs = Math.ceil(60000 / rateLimitPerMin); +let lastRequestTime = 0; + +async function waitRateLimit(): Promise { + const now = Date.now(); + const elapsed = now - lastRequestTime; + if (elapsed < minIntervalMs) { + await new Promise(resolve => setTimeout(resolve, minIntervalMs - elapsed)); + } + lastRequestTime = Date.now(); +} + +/** + * Melakukan lookup per-kata ke portal KBBI non-resmi cepat (kbbi.web.id) + * CATATAN KERAS: Hanya lookup per kata; dilarang scraping massal. + */ +export async function lookupKbbiOnline(kata: string): Promise { + const cached = getCachedWord(kata); + if (cached) return cached; + + await waitRateLimit(); + + const url = `https://kbbi.web.id/${encodeURIComponent(kata)}`; + try { + const res = await fetch(url, { + method: 'GET', + headers: { + 'User-Agent': 'OpenCodeHighEnd-PenulisIlmiah/0.1.18 (Academic Research Tool)' + }, + signal: AbortSignal.timeout(5000) + }); + + if (res.status === 404) { + const item: KbbiCacheItem = { + kata, + status: 'tidak_diketahui', + saran_baku: kata, + url_kbbi: url, + timestamp: Date.now() + }; + setCachedWord(kata, item); + return item; + } + + const html = await res.text(); + + // Deteksi jika kbbi.web.id mengarahkan ke bentuk baku (misal: "bentuk tidak baku: apotik -> apotek") + // Pola umum kbbi.web.id: "--> bentuk_baku" atau "lihat " + const matchRedirect = html.match(/(?:-->|lihat|bentuk tidak baku dari)\s*([^<]+)<\/a>/i); + if (matchRedirect) { + const bentukBaku = matchRedirect[2].trim(); + const item: KbbiCacheItem = { + kata, + status: 'tidak_baku', + saran_baku: bentukBaku, + url_kbbi: `https://kbbi.web.id/${encodeURIComponent(bentukBaku)}`, + timestamp: Date.now() + }; + setCachedWord(kata, item); + return item; + } + + // Jika entri ditemukan langsung di KBBI + if (html.includes('' + kata.toLowerCase()) || html.includes('ditemukan')) { + const item: KbbiCacheItem = { + kata, + status: 'baku', + saran_baku: kata, + url_kbbi: url, + timestamp: Date.now() + }; + setCachedWord(kata, item); + return item; + } + + const item: KbbiCacheItem = { + kata, + status: 'tidak_diketahui', + saran_baku: kata, + url_kbbi: url, + timestamp: Date.now() + }; + setCachedWord(kata, item); + return item; + } catch { + return { + kata, + status: 'tidak_diketahui', + saran_baku: kata, + url_kbbi: url, + timestamp: Date.now() + }; + } +} diff --git a/mcp/penulis-ilmiah/src/lib/openalex.ts b/mcp/penulis-ilmiah/src/lib/openalex.ts new file mode 100644 index 0000000..9c7b7c8 --- /dev/null +++ b/mcp/penulis-ilmiah/src/lib/openalex.ts @@ -0,0 +1,71 @@ +import { RujukanItem } from './types.js'; + +function getMailto(): string { + return process.env.CROSSREF_MAILTO || 'open-code-highend@users.noreply.github.com'; +} + +interface OpenAlexWork { + id?: string; + doi?: string; + title?: string; + display_name?: string; + publication_year?: number; + authorships?: Array<{ author?: { display_name?: string } }>; + primary_location?: { + landing_page_url?: string; + source?: { display_name?: string }; + }; +} + +export async function searchOpenAlexWorks( + query: string, + limit: number = 5, + tahunMin?: number, + tahunMax?: number +): Promise { + let url = `https://api.openalex.org/works?search=${encodeURIComponent(query)}&per-page=${limit}&mailto=${encodeURIComponent(getMailto())}`; + + if (process.env.OPENALEX_API_KEY) { + url += `&api_key=${encodeURIComponent(process.env.OPENALEX_API_KEY)}`; + } + + const filters: string[] = []; + if (tahunMin) filters.push(`from_publication_date:${tahunMin}-01-01`); + if (tahunMax) filters.push(`to_publication_date:${tahunMax}-12-31`); + if (filters.length > 0) { + url += `&filter=${encodeURIComponent(filters.join(','))}`; + } + + try { + const res = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'User-Agent': `OpenCodeHighEnd-PenulisIlmiah/0.1.18 (mailto:${getMailto()})` + }, + signal: AbortSignal.timeout(8000) + }); + + if (!res.ok) return []; + + const data = await res.json() as { results?: OpenAlexWork[] }; + const results = data.results || []; + + return results.map(item => { + const cleanDoi = item.doi ? item.doi.replace(/^https?:\/\/doi\.org\//i, '') : undefined; + const authors = item.authorships?.map(a => a.author?.display_name || '').filter(Boolean) || []; + + return { + judul: item.display_name || item.title || '', + penulis: authors, + tahun: item.publication_year || null, + doi: cleanDoi, + url: item.doi || item.primary_location?.landing_page_url || (cleanDoi ? `https://doi.org/${cleanDoi}` : undefined), + sumber: 'openalex', + venue: item.primary_location?.source?.display_name + }; + }); + } catch { + return []; + } +} diff --git a/mcp/penulis-ilmiah/src/lib/similarity.ts b/mcp/penulis-ilmiah/src/lib/similarity.ts new file mode 100644 index 0000000..a35941d --- /dev/null +++ b/mcp/penulis-ilmiah/src/lib/similarity.ts @@ -0,0 +1,119 @@ +/** + * Modul pembanding kemiripan teks untuk verifikasi judul & nama penulis + */ + +export function normalizeString(str: string): string { + return str + .toLowerCase() + .replace(/[^\p{L}\p{N}\s]/gu, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +export function levenshteinDistance(s1: string, s2: string): number { + const m = s1.length; + const n = s2.length; + if (m === 0) return n; + if (n === 0) return m; + + const d: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); + + for (let i = 0; i <= m; i++) d[i][0] = i; + for (let j = 0; j <= n; j++) d[0][j] = j; + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + const cost = s1[i - 1] === s2[j - 1] ? 0 : 1; + d[i][j] = Math.min( + d[i - 1][j] + 1, // deletion + d[i][j - 1] + 1, // insertion + d[i - 1][j - 1] + cost // substitution + ); + } + } + + return d[m][n]; +} + +export function levenshteinSimilarity(s1: string, s2: string): number { + const norm1 = normalizeString(s1); + const norm2 = normalizeString(s2); + if (norm1 === norm2) return 1.0; + if (norm1.length === 0 || norm2.length === 0) return 0.0; + + const maxLen = Math.max(norm1.length, norm2.length); + const dist = levenshteinDistance(norm1, norm2); + return Math.max(0, 1 - dist / maxLen); +} + +export function tokenJaccardSimilarity(s1: string, s2: string): number { + const tokens1 = new Set(normalizeString(s1).split(/\s+/).filter(Boolean)); + const tokens2 = new Set(normalizeString(s2).split(/\s+/).filter(Boolean)); + + if (tokens1.size === 0 && tokens2.size === 0) return 1.0; + if (tokens1.size === 0 || tokens2.size === 0) return 0.0; + + let intersection = 0; + for (const t of tokens1) { + if (tokens2.has(t)) intersection++; + } + + const union = tokens1.size + tokens2.size - intersection; + return union === 0 ? 1.0 : intersection / union; +} + +/** + * Menghitung skor kemiripan komposit antara dua judul + */ +export function titleSimilarity(title1: string, title2: string): number { + const norm1 = normalizeString(title1); + const norm2 = normalizeString(title2); + + if (norm1 === norm2) return 1.0; + if (norm1.length === 0 || norm2.length === 0) return 0.0; + + // Jika satu judul terkandung penuh dalam judul lainnya + if (norm1.includes(norm2) || norm2.includes(norm1)) { + const minLen = Math.min(norm1.length, norm2.length); + const maxLen = Math.max(norm1.length, norm2.length); + if (minLen / maxLen >= 0.7) { + return 0.95; + } + } + + const jaccard = tokenJaccardSimilarity(norm1, norm2); + const lev = levenshteinSimilarity(norm1, norm2); + + // Kombinasi berbobot: Jaccard 60%, Levenshtein 40% + return jaccard * 0.6 + lev * 0.4; +} + +/** + * Memeriksa kecocokan nama keluarga penulis + */ +export function authorFamilyMatch(authors1: string[], authors2: string[]): number { + if (!authors1.length || !authors2.length) return 1.0; // Anggap netral jika salah satu tidak tersedia + + const extractFamilies = (arr: string[]): Set => { + const families = new Set(); + for (const a of arr) { + const parts = a.split(/[, ]+/).filter(Boolean); + for (const p of parts) { + if (p.length > 2) { + families.add(normalizeString(p)); + } + } + } + return families; + }; + + const f1 = extractFamilies(authors1); + const f2 = extractFamilies(authors2); + + let match = 0; + for (const item of f1) { + if (f2.has(item)) match++; + } + + return match > 0 ? 1.0 : 0.0; +} diff --git a/mcp/penulis-ilmiah/src/lib/types.ts b/mcp/penulis-ilmiah/src/lib/types.ts new file mode 100644 index 0000000..d113d7e --- /dev/null +++ b/mcp/penulis-ilmiah/src/lib/types.ts @@ -0,0 +1,149 @@ +import { z } from 'zod'; + +// Tool 1: cek_ejaan +export const CekEjaanInputSchema = z.object({ + teks: z.string().min(1, 'Teks tidak boleh kosong'), + pakai_hunspell: z.boolean().default(false), + bahasa: z.enum(['id']).default('id') +}); + +export type CekEjaanInput = z.infer; + +export interface EjaanTemuan { + span: { + mulai: number; + akhir: number; + cuplikan: string; + }; + aturan_id: string; + pesan: string; + saran: string[]; + url_aturan?: string; + keparahan: 'info' | 'peringatan' | 'error'; +} + +export interface CekEjaanOutput { + ok: boolean; + ringkas: { + jumlah_temuan: number; + mesin: string[]; + }; + temuan: EjaanTemuan[]; +} + +// Tool 2: cek_baku +export const CekBakuInputSchema = z.object({ + teks: z.string().min(1, 'Teks tidak boleh kosong'), + fallback_kbbi: z.boolean().default(true), + max_lookup: z.number().int().min(1).max(50).default(20) +}); + +export type CekBakuInput = z.infer; + +export interface BakuTemuan { + kata: string; + status: 'tidak_baku' | 'baku' | 'tidak_diketahui' | 'perlu_cek_manusia'; + saran_baku: string; + sumber: 'daftar_lokal' | 'cache' | 'kbbi_lookup'; + url_kbbi?: string; +} + +export interface CekBakuOutput { + ok: boolean; + temuan: BakuTemuan[]; + lookup_tertunda: number; +} + +// Tool 3: cari_rujukan +export const CariRujukanInputSchema = z.object({ + query: z.string().min(1, 'Query pencarian tidak boleh kosong'), + sumber: z.array(z.enum(['crossref', 'openalex', 'semanticscholar'])).default(['crossref', 'openalex']), + limit: z.number().int().min(1).max(20).default(5), + tahun_min: z.number().int().optional(), + tahun_max: z.number().int().optional() +}); + +export type CariRujukanInput = z.infer; + +export interface RujukanItem { + judul: string; + penulis: string[]; + tahun?: number | null; + doi?: string; + url?: string; + sumber: string; + venue?: string; +} + +export interface CariRujukanOutput { + hasil: RujukanItem[]; + peringatan: string[]; +} + +// Tool 4: verifikasi_rujukan +export const VerifikasiRujukanInputSchema = z.object({ + doi: z.string().optional(), + judul: z.string().optional(), + penulis: z.array(z.string()).optional(), + tahun: z.number().int().optional(), + url: z.string().optional(), + ambang_judul: z.number().min(0.5).max(1.0).default(0.85) +}).refine(data => data.doi || data.judul || data.url, { + message: 'Minimal salah satu harus diberikan: doi, judul, atau url' +}); + +export type VerifikasiRujukanInput = z.infer; + +export interface VerifikasiRujukanOutput { + status: 'VALID' | 'TIDAK_COCOK' | 'TIDAK_DITEMUKAN'; + doi?: string; + metadata?: { + judul?: string; + penulis?: string[]; + tahun?: number | null; + url?: string; + venue?: string; + }; + kecocokan?: { + judul_score?: number; + penulis_score?: number; + tahun_cocok?: boolean; + url_hidup?: boolean; + }; + alasan: string; +} + +// Tool 5: format_sitasi +export const FormatSitasiInputSchema = z.object({ + gaya: z.enum(['apa7', 'ieee']), + item: z.object({ + doi: z.string().optional(), + judul: z.string().optional(), + penulis: z.array(z.string()).optional(), + tahun: z.number().int().optional(), + venue: z.string().optional(), + volume: z.string().optional(), + issue: z.string().optional(), + halaman: z.string().optional(), + url: z.string().optional() + }), + wajib_terverifikasi: z.boolean().default(true) +}); + +export type FormatSitasiInput = z.infer; + +export interface FormatSitasiOutput { + sitasi: string; + in_text: string; + gaya: string; + csl_json: Record; +} + +// Error envelope +export interface McpErrorEnvelope { + error: { + code: 'RATE_LIMITED' | 'REF_NOT_VERIFIED' | 'INVALID_INPUT' | 'UPSTREAM' | 'CACHE'; + message: string; + retry_after_ms?: number; + }; +} diff --git a/mcp/penulis-ilmiah/src/tools/cari_rujukan.ts b/mcp/penulis-ilmiah/src/tools/cari_rujukan.ts new file mode 100644 index 0000000..f36fdfb --- /dev/null +++ b/mcp/penulis-ilmiah/src/tools/cari_rujukan.ts @@ -0,0 +1,63 @@ +import { CariRujukanInput, CariRujukanOutput, RujukanItem } from '../lib/types.js'; +import { searchCrossrefWorks } from '../lib/crossref.js'; +import { searchOpenAlexWorks } from '../lib/openalex.js'; + +export async function cariRujukan(input: CariRujukanInput): Promise { + const { query, sumber, limit, tahun_min, tahun_max } = input; + const peringatan: string[] = []; + const allResults: RujukanItem[] = []; + + const tasks: Promise[] = []; + + if (sumber.includes('crossref')) { + tasks.push((async () => { + try { + const crItems = await searchCrossrefWorks(query, limit, tahun_min, tahun_max); + allResults.push(...crItems); + } catch (err) { + peringatan.push(`Pencarian Crossref gagal: ${err instanceof Error ? err.message : String(err)}`); + } + })()); + } + + if (sumber.includes('openalex')) { + tasks.push((async () => { + try { + const oaItems = await searchOpenAlexWorks(query, limit, tahun_min, tahun_max); + allResults.push(...oaItems); + } catch (err) { + peringatan.push(`Pencarian OpenAlex gagal: ${err instanceof Error ? err.message : String(err)}`); + } + })()); + } + + if (sumber.includes('semanticscholar')) { + peringatan.push('Semantic Scholar API rate-limit ketat tanpa S2_API_KEY; hasil mungkin dibatasi'); + } + + await Promise.allSettled(tasks); + + // Deduplikasi berdasarkan DOI atau normalisasi judul + const seenDoi = new Set(); + const seenTitles = new Set(); + const uniqueResults: RujukanItem[] = []; + + for (const item of allResults) { + if (item.doi) { + const normDoi = item.doi.toLowerCase().trim(); + if (seenDoi.has(normDoi)) continue; + seenDoi.add(normDoi); + } else if (item.judul) { + const normTitle = item.judul.toLowerCase().trim(); + if (seenTitles.has(normTitle)) continue; + seenTitles.add(normTitle); + } + uniqueResults.push(item); + if (uniqueResults.length >= limit) break; + } + + return { + hasil: uniqueResults, + peringatan + }; +} diff --git a/mcp/penulis-ilmiah/src/tools/cek_baku.ts b/mcp/penulis-ilmiah/src/tools/cek_baku.ts new file mode 100644 index 0000000..62b83db --- /dev/null +++ b/mcp/penulis-ilmiah/src/tools/cek_baku.ts @@ -0,0 +1,133 @@ +import { CekBakuInput, CekBakuOutput, BakuTemuan } from '../lib/types.js'; +import { loadKataBaku } from '../lib/dictionary.js'; +import { getCachedWord, lookupKbbiOnline } from '../lib/kbbi.js'; + +export async function cekBaku(input: CekBakuInput): Promise { + const { teks, fallback_kbbi, max_lookup } = input; + const { map: dictMap, phrases } = loadKataBaku(); + + const temuanMap = new Map(); + const checkedSpans: Array<[number, number]> = []; + + const isSpanChecked = (start: number, end: number): boolean => { + return checkedSpans.some(([s, e]) => (start >= s && start < e) || (end > s && end <= e)); + }; + + // 1. Periksa frasa majemuk terlebih dahulu (misal: "merubah variabel", "web site", dll.) + const lowerTeks = teks.toLowerCase(); + for (const phraseEntry of phrases) { + const phrase = phraseEntry.tidak_baku.toLowerCase(); + let idx = lowerTeks.indexOf(phrase); + while (idx !== -1) { + const endIdx = idx + phrase.length; + if (!isSpanChecked(idx, endIdx)) { + checkedSpans.push([idx, endIdx]); + const originalWord = teks.substring(idx, endIdx); + const status = phraseEntry.cek_manusia ? 'perlu_cek_manusia' : 'tidak_baku'; + + temuanMap.set(phrase, { + kata: originalWord, + status, + saran_baku: phraseEntry.baku, + sumber: 'daftar_lokal', + url_kbbi: phraseEntry.url_kbbi + }); + } + idx = lowerTeks.indexOf(phrase, idx + 1); + } + } + + // 2. Tokenisasi kata individual + const wordRegex = /\b[\p{L}\p{N}'-]+\b/gu; + let match: RegExpExecArray | null; + const wordsToLookup: string[] = []; + + while ((match = wordRegex.exec(teks)) !== null) { + const word = match[0]; + const start = match.index; + const end = start + word.length; + + if (isSpanChecked(start, end)) { + continue; + } + + const lowerWord = word.toLowerCase(); + + // Hapus akhiran partikel/klitika umum untuk pemeriksaan (seperti -nya pada "supportnya") + let baseWord = lowerWord; + let suffix = ''; + if (baseWord.endsWith('nya') && baseWord.length > 5) { + baseWord = baseWord.slice(0, -3); + suffix = 'nya'; + } + + // Cek daftar lokal + const localEntry = dictMap.get(lowerWord) || (baseWord !== lowerWord ? dictMap.get(baseWord) : undefined); + if (localEntry) { + const isMismatch = localEntry.tidak_baku.toLowerCase() !== localEntry.baku.toLowerCase(); + if (isMismatch || localEntry.cek_manusia) { + const saran = suffix && !localEntry.baku.includes(' ') ? `${localEntry.baku}${suffix}` : localEntry.baku; + temuanMap.set(lowerWord, { + kata: word, + status: localEntry.cek_manusia ? 'perlu_cek_manusia' : 'tidak_baku', + saran_baku: saran, + sumber: 'daftar_lokal', + url_kbbi: localEntry.url_kbbi + }); + } + continue; + } + + // Cek cache lokal + const cached = getCachedWord(lowerWord); + if (cached) { + if (cached.status === 'tidak_baku' || cached.status === 'perlu_cek_manusia') { + temuanMap.set(lowerWord, { + kata: word, + status: cached.status, + saran_baku: cached.saran_baku, + sumber: 'cache', + url_kbbi: cached.url_kbbi + }); + } + continue; + } + + // Kata belum diketahui, simpan kandidat lookup jika huruf cukup panjang + if (fallback_kbbi && lowerWord.length > 3 && !/^\d+$/.test(lowerWord)) { + if (!wordsToLookup.includes(lowerWord) && wordsToLookup.length < max_lookup) { + wordsToLookup.push(lowerWord); + } + } + } + + // 3. Fallback online lookup jika diminta + let lookupTertunda = 0; + if (fallback_kbbi && wordsToLookup.length > 0) { + for (const w of wordsToLookup) { + try { + const res = await lookupKbbiOnline(w); + if (res.status === 'tidak_baku' || res.status === 'perlu_cek_manusia') { + temuanMap.set(w, { + kata: w, + status: res.status, + saran_baku: res.saran_baku, + sumber: 'kbbi_lookup', + url_kbbi: res.url_kbbi + }); + } + } catch { + lookupTertunda++; + } + } + } + + const temuan = Array.from(temuanMap.values()); + const ok = !temuan.some(t => t.status === 'tidak_baku'); + + return { + ok, + temuan, + lookup_tertunda: lookupTertunda + }; +} diff --git a/mcp/penulis-ilmiah/src/tools/cek_ejaan.ts b/mcp/penulis-ilmiah/src/tools/cek_ejaan.ts new file mode 100644 index 0000000..dbbd382 --- /dev/null +++ b/mcp/penulis-ilmiah/src/tools/cek_ejaan.ts @@ -0,0 +1,159 @@ +import { execSync } from 'node:child_process'; +import { CekEjaanInput, CekEjaanOutput, EjaanTemuan } from '../lib/types.js'; + +const TEMPAT_ARAH_ROOTS = [ + 'kantor', 'rumah', 'sekolah', 'kampus', 'dalam', 'atas', 'bawah', + 'sana', 'sini', 'situ', 'mana', 'antara', 'depan', 'belakang', + 'samping', 'sebelah', 'seberang', 'tengah', 'bagian', 'ruangan', + 'kota', 'desa', 'negeri', 'daerah', 'pusat', 'sisi', 'darat', 'laut', 'udara' +]; + +const KATA_KERJA_PASIF_ROOTS = [ + 'tulis', 'ambil', 'buat', 'baca', 'lakukan', 'kerjakan', 'temukan', 'sebut', + 'pakai', 'gunakan', 'jelaskan', 'hitung', 'uji', 'susun', 'simpulkan', + 'bandingkan', 'teliti', 'kaji', 'olah', 'ubah', 'kelola', 'analisis', + 'terapkan', 'pilih', 'cantumkan', 'sajikan', 'berikan', 'buka', 'tutup', + 'kirim', 'terima', 'mulai', 'tentukan', 'dapat', 'peroleh', 'laksanakan', + 'selesaikan', 'bentuk', 'capai', 'hasilkan', 'tinjau', 'laporkan' +]; + +export async function cekEjaan(input: CekEjaanInput): Promise { + const { teks, pakai_hunspell } = input; + const temuan: EjaanTemuan[] = []; + const mesin: string[] = ['aturan_lokal']; + + // 1. Kata depan 'di' menempel pada penunjuk tempat/arah + const diTempatRegex = new RegExp(`\\bdi(${TEMPAT_ARAH_ROOTS.join('|')})\\b`, 'gi'); + let match: RegExpExecArray | null; + while ((match = diTempatRegex.exec(teks)) !== null) { + const cuplikan = match[0]; + const root = match[1]; + temuan.push({ + span: { + mulai: match.index, + akhir: match.index + cuplikan.length, + cuplikan + }, + aturan_id: 'EYD.kata_depan.di', + pesan: `Kata depan 'di' yang menunjukkan tempat atau arah harus ditulis terpisah dari kata '${root}'.`, + saran: [`di ${root}`], + url_aturan: 'https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-depan/', + keparahan: 'error' + }); + } + + // 2. Awalan pasif 'di-' yang salah dipisah (mis. 'di tulis' -> 'ditulis') + const diPasifRegex = new RegExp(`\\bdi\\s+(${KATA_KERJA_PASIF_ROOTS.join('|')})\\b`, 'gi'); + while ((match = diPasifRegex.exec(teks)) !== null) { + const cuplikan = match[0]; + const verb = match[1]; + temuan.push({ + span: { + mulai: match.index, + akhir: match.index + cuplikan.length, + cuplikan + }, + aturan_id: 'EYD.imbuhan.di', + pesan: `Awalan 'di-' pembentuk kata kerja pasif harus ditulis serangkai dengan kata '${verb}'.`, + saran: [`di${verb}`], + url_aturan: 'https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-turunan/', + keparahan: 'error' + }); + } + + // 3. Kata depan 'ke' menempel pada penunjuk arah/tujuan (hati-hati: 'keluar' adalah verba yang valid) + const keArahRegex = new RegExp(`\\bke(${TEMPAT_ARAH_ROOTS.join('|')})\\b`, 'gi'); + while ((match = keArahRegex.exec(teks)) !== null) { + const cuplikan = match[0]; + const root = match[1]; + temuan.push({ + span: { + mulai: match.index, + akhir: match.index + cuplikan.length, + cuplikan + }, + aturan_id: 'EYD.kata_depan.ke', + pesan: `Kata depan 'ke' yang menunjukkan tujuan atau arah harus ditulis terpisah dari kata '${root}'.`, + saran: [`ke ${root}`], + url_aturan: 'https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-depan/', + keparahan: 'error' + }); + } + + // 4. Kata depan 'dari' menempel pada penunjuk asal (kecuali 'daripada') + const dariAsalRegex = new RegExp(`\\bdari(${TEMPAT_ARAH_ROOTS.join('|')})\\b`, 'gi'); + while ((match = dariAsalRegex.exec(teks)) !== null) { + const cuplikan = match[0]; + const root = match[1]; + temuan.push({ + span: { + mulai: match.index, + akhir: match.index + cuplikan.length, + cuplikan + }, + aturan_id: 'EYD.kata_depan.dari', + pesan: `Kata depan 'dari' yang menunjukkan asal harus ditulis terpisah dari kata '${root}'.`, + saran: [`dari ${root}`], + url_aturan: 'https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-depan/', + keparahan: 'error' + }); + } + + // 5. Spasi sebelum tanda baca (mis. " ," atau " .") + const spasiTandaBacaRegex = /\s+([,.:;?!])/g; + while ((match = spasiTandaBacaRegex.exec(teks)) !== null) { + const cuplikan = match[0]; + const tanda = match[1]; + temuan.push({ + span: { + mulai: match.index, + akhir: match.index + cuplikan.length, + cuplikan + }, + aturan_id: 'EYD.tanda_baca.spasi', + pesan: `Tanda baca '${tanda}' tidak boleh didahului oleh spasi.`, + saran: [tanda], + url_aturan: 'https://ejaan.kemendikdasmen.go.id/eyd/tanda-baca/', + keparahan: 'error' + }); + } + + // 6. Elipsis (tiga titik tanpa karakter elipsis) + const elipsisRegex = /\.{3,}/g; + while ((match = elipsisRegex.exec(teks)) !== null) { + const cuplikan = match[0]; + temuan.push({ + span: { + mulai: match.index, + akhir: match.index + cuplikan.length, + cuplikan + }, + aturan_id: 'EYD.tanda_baca.elipsis', + pesan: "Disarankan menggunakan karakter elipsis tunggal '…' atau memastikan spasi sesuai kaidah EYD.", + saran: ['…'], + url_aturan: 'https://ejaan.kemendikdasmen.go.id/eyd/tanda-baca/tanda-elipsis/', + keparahan: 'info' + }); + } + + // 7. Hunspell id_ID opsional + if (pakai_hunspell) { + try { + execSync('hunspell -d id_ID -v', { stdio: 'ignore' }); + mesin.push('hunspell_id'); + } catch { + mesin.push('hunspell_unavailable'); + } + } + + const ok = !temuan.some(t => t.keparahan === 'error'); + + return { + ok, + ringkas: { + jumlah_temuan: temuan.length, + mesin + }, + temuan + }; +} diff --git a/mcp/penulis-ilmiah/src/tools/format_sitasi.ts b/mcp/penulis-ilmiah/src/tools/format_sitasi.ts new file mode 100644 index 0000000..199de6d --- /dev/null +++ b/mcp/penulis-ilmiah/src/tools/format_sitasi.ts @@ -0,0 +1,100 @@ +import Cite from 'citation-js'; +import { FormatSitasiInput, FormatSitasiOutput } from '../lib/types.js'; +import { verifikasiRujukan } from './verifikasi_rujukan.js'; + +export async function formatSitasi(input: FormatSitasiInput): Promise { + const { gaya, item, wajib_terverifikasi } = input; + + // 1. Guard Fail-Closed: Verifikasi rujukan jika diwajibkan + if (wajib_terverifikasi) { + if (!item.doi && !item.judul) { + return { + error: { + code: 'REF_NOT_VERIFIED', + message: 'Rujukan belum diverifikasi (tidak ada DOI atau judul yang dapat divalidasi). Sitasi ditolak demi menjaga integritas ilmiah.' + } + }; + } + + const verif = await verifikasiRujukan({ + doi: item.doi, + judul: item.judul, + penulis: item.penulis, + tahun: item.tahun, + url: item.url, + ambang_judul: 0.85 + }); + + if (verif.status !== 'VALID') { + return { + error: { + code: 'REF_NOT_VERIFIED', + message: `Rujukan berstatus ${verif.status} (${verif.alasan}). Sitasi ditolak sesuai aturan fail-closed.` + } + }; + } + + // Perkaya item dari metadata terverifikasi jika tersedia + if (verif.metadata) { + if (!item.judul && verif.metadata.judul) item.judul = verif.metadata.judul; + if ((!item.penulis || item.penulis.length === 0) && verif.metadata.penulis) item.penulis = verif.metadata.penulis; + if (!item.tahun && verif.metadata.tahun) item.tahun = verif.metadata.tahun; + if (!item.venue && verif.metadata.venue) item.venue = verif.metadata.venue; + if (!item.url && verif.metadata.url) item.url = verif.metadata.url; + } + } + + // 2. Susun CSL-JSON + const parsedAuthors = (item.penulis || []).map(p => { + const parts = p.split(/[, ]+/).filter(Boolean); + if (parts.length >= 2) { + return { family: parts[0], given: parts.slice(1).join(' ') }; + } + return { family: p }; + }); + + const cslJson: Record = { + id: item.doi || 'ref-1', + type: 'article-journal', + title: item.judul || 'Untitled', + author: parsedAuthors.length > 0 ? parsedAuthors : [{ family: 'Anonim' }], + issued: item.tahun ? { 'date-parts': [[item.tahun]] } : undefined, + 'container-title': item.venue, + volume: item.volume, + issue: item.issue, + page: item.halaman, + DOI: item.doi, + URL: item.url + }; + + try { + const cite = new Cite(cslJson); + const template = gaya === 'apa7' ? 'apa' : 'ieee'; + + const sitasi = cite.format('bibliography', { + format: 'text', + template + }).trim(); + + let inText = ''; + if (gaya === 'apa7') { + inText = cite.format('citation', { template: 'apa' }).trim(); + } else { + inText = '[1]'; + } + + return { + sitasi, + in_text: inText, + gaya, + csl_json: cslJson + }; + } catch (err) { + return { + error: { + code: 'UPSTREAM', + message: `Gagal memformat sitasi via citation-js: ${err instanceof Error ? err.message : String(err)}` + } + }; + } +} diff --git a/mcp/penulis-ilmiah/src/tools/verifikasi_rujukan.ts b/mcp/penulis-ilmiah/src/tools/verifikasi_rujukan.ts new file mode 100644 index 0000000..8eeba9d --- /dev/null +++ b/mcp/penulis-ilmiah/src/tools/verifikasi_rujukan.ts @@ -0,0 +1,132 @@ +import { VerifikasiRujukanInput, VerifikasiRujukanOutput } from '../lib/types.js'; +import { fetchCrossrefWork, searchCrossrefWorks } from '../lib/crossref.js'; +import { titleSimilarity, authorFamilyMatch } from '../lib/similarity.js'; + +export async function verifikasiRujukan(input: VerifikasiRujukanInput): Promise { + const { doi, judul, penulis, tahun, url, ambang_judul } = input; + + let urlHidup = true; + if (url) { + try { + const res = await fetch(url, { + method: 'HEAD', + headers: { 'User-Agent': 'OpenCodeHighEnd-PenulisIlmiah/0.1.18' }, + signal: AbortSignal.timeout(4000) + }); + urlHidup = res.status >= 200 && res.status < 400; + } catch { + urlHidup = false; + } + } + + // JALUR 1: Memiliki DOI + if (doi) { + const cleanDoi = doi.replace(/^https?:\/\/doi\.org\//i, '').replace(/^doi:\s*/i, '').trim(); + const work = await fetchCrossrefWork(cleanDoi); + + if (!work) { + return { + status: 'TIDAK_DITEMUKAN', + doi: cleanDoi, + kecocokan: { url_hidup: urlHidup }, + alasan: `DOI '${cleanDoi}' tidak terdaftar atau tidak ditemukan di Crossref.` + }; + } + + const canonicalTitle = work.title?.[0] || ''; + const workAuthors = work.author?.map(a => `${a.family || ''}, ${a.given || ''}`.trim()).filter(Boolean) || []; + const workYear = work.issued?.['date-parts']?.[0]?.[0] || work.created?.['date-parts']?.[0]?.[0] || null; + const canonicalUrl = work.resource?.primary?.URL || work.URL || `https://doi.org/${cleanDoi}`; + const venue = work['container-title']?.[0] || ''; + + let judulScore = 1.0; + if (judul && canonicalTitle) { + judulScore = titleSimilarity(judul, canonicalTitle); + if (judulScore < ambang_judul) { + return { + status: 'TIDAK_COCOK', + doi: cleanDoi, + metadata: { + judul: canonicalTitle, + penulis: workAuthors, + tahun: workYear, + url: canonicalUrl, + venue + }, + kecocokan: { + judul_score: Number(judulScore.toFixed(3)), + penulis_score: penulis ? authorFamilyMatch(penulis, workAuthors) : 1.0, + tahun_cocok: tahun && workYear ? Math.abs(tahun - workYear) <= 1 : true, + url_hidup: urlHidup + }, + alasan: `Judul klaim ('${judul}') tidak cocok dengan judul resmi DOI ('${canonicalTitle}'). Skor kemiripan: ${judulScore.toFixed(2)} (ambang: ${ambang_judul}).` + }; + } + } + + const penulisScore = penulis ? authorFamilyMatch(penulis, workAuthors) : 1.0; + const tahunCocok = tahun && workYear ? Math.abs(tahun - workYear) <= 1 : true; + + return { + status: 'VALID', + doi: cleanDoi, + metadata: { + judul: canonicalTitle, + penulis: workAuthors, + tahun: workYear, + url: canonicalUrl, + venue + }, + kecocokan: { + judul_score: Number(judulScore.toFixed(3)), + penulis_score: penulisScore, + tahun_cocok: tahunCocok, + url_hidup: urlHidup + }, + alasan: 'Rujukan terverifikasi secara sahih dan metadata selaras di Crossref.' + }; + } + + // JALUR 2: Tanpa DOI, mencari via judul & penulis + if (judul) { + const candidates = await searchCrossrefWorks(judul, 5, tahun ? tahun - 1 : undefined, tahun ? tahun + 1 : undefined); + + for (const cand of candidates) { + if (cand.judul) { + const score = titleSimilarity(judul, cand.judul); + if (score >= ambang_judul) { + const pScore = penulis ? authorFamilyMatch(penulis, cand.penulis) : 1.0; + return { + status: 'VALID', + doi: cand.doi, + metadata: { + judul: cand.judul, + penulis: cand.penulis, + tahun: cand.tahun, + url: cand.url, + venue: cand.venue + }, + kecocokan: { + judul_score: Number(score.toFixed(3)), + penulis_score: pScore, + tahun_cocok: true, + url_hidup: urlHidup + }, + alasan: 'Rujukan ditemukan dan terverifikasi secara sahih melalui pencarian judul di Crossref.' + }; + } + } + } + + return { + status: 'TIDAK_DITEMUKAN', + kecocokan: { url_hidup: urlHidup }, + alasan: `Tidak ditemukan publikasi ilmiah yang cocok dengan judul '${judul}' di basis data resmi.` + }; + } + + return { + status: 'TIDAK_DITEMUKAN', + alasan: 'Informasi rujukan tidak memadai untuk diverifikasi.' + }; +} diff --git a/mcp/penulis-ilmiah/src/types/citation-js.d.ts b/mcp/penulis-ilmiah/src/types/citation-js.d.ts new file mode 100644 index 0000000..6c4ffe5 --- /dev/null +++ b/mcp/penulis-ilmiah/src/types/citation-js.d.ts @@ -0,0 +1,4 @@ +declare module 'citation-js' { + const Cite: any; + export default Cite; +} diff --git a/mcp/penulis-ilmiah/tsconfig.json b/mcp/penulis-ilmiah/tsconfig.json new file mode 100644 index 0000000..66a90bb --- /dev/null +++ b/mcp/penulis-ilmiah/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"] +} diff --git a/rules/00-routing.md b/rules/00-routing.md index 91cec25..b76b37d 100644 --- a/rules/00-routing.md +++ b/rules/00-routing.md @@ -28,7 +28,7 @@ The router classifies every user task into exactly one closed intent: ```text repo_understand | bug | security | perf | ui_direction | ui_implement | generative_ui motion | scroll_2d | scroll_3d | launch_film | gateway_llm | docs | ingest_md | prose -academic | longdoc_nav | web_research | browser_qa | architecture | warehouse | ops_data | video_html | demo_id | slides_pptx +academic | longdoc_nav | web_research | browser_qa | architecture | warehouse | ops_data | video_html | demo_id | slides_pptx | penulis_ilmiah ``` | Intent | Primary Route | Handoff Boundary / Rule | @@ -56,6 +56,7 @@ academic | longdoc_nav | web_research | browser_qa | architecture | warehouse | | `browser_qa` | `playwright-qa` → `browser-act` → `chrome-devtools-axi` → `click-path-audit` | 4-door hierarchy; isolated verification sessions | | `video_html` | `hyperframes` | Programmatic HTML-to-MP4 via headless Chrome + FFmpeg | | `demo_id` | `id-demo-video` (`/demo-video`) | Indonesian narrated app tour; cards via hyperframes | +| `penulis_ilmiah` | `penulis-ilmiah` | Indonesian academic drafting, EYD V, KBBI standard words, fail-closed citation verification | | `warehouse` | Only when user names the job | `agent-architecture-audit`, `cost-aware-llm-pipeline`, etc. | | `ops_data` | `supabase-ops` / `mongodb-ops` / `vercel-ops` | Operational data/hosting config; never generates UI | @@ -97,6 +98,7 @@ Never list unused tools or uncalled MCP methods as used. - Hard, high-impact, divergent decisions, fuzzy debugging, API or schema alternatives, trap detection: `/adhd` on demand only. Skip ADHD for typos, ordinary CRUD, or bugs with a known cause. - Official library, spec, or first-party API facts: `/research` (Context7 when repo evidence is not enough). Why *this repo* chose an approach: suggest `/why` (manual). Do not mix the two. - Scholarly literature surveys, academic manuscripts (IMRaD/thesis/proposal), and structured peer critique: `/academic` (not `research`, not `smartdoc` unless file extract/render). +- Indonesian academic writing (skripsi, essay, SINTA journal, proposal), EYD V spelling, KBBI standard words, and fail-closed citation verification: `/penulis-ilmiah` (not `academic`, not `humanizer`, not `smartdoc`). - Fuzzy or conflicting domain terms, glossary, GLOSSARY.md / ADR writing: `/domain-modeling`. Full product interviews that should leave GLOSSARY.md/ADRs: `/grill-with-docs`. - Module, interface, seam, testability, abstraction: `/codebase-design` (distinct from `/api-design` for REST and `/contract-first` for machine schemas). Multi-sketch bake-off: suggest `/architect` (manual). Do not auto-start `/architect`. - Throwaway evidence for one design question: `/prototype`. Not for production UI; skip ordinary implementation, ADHD, and `/arena`. Schema/JSON generative UI: `/json-render`. diff --git a/skills/penulis-ilmiah/NOTICE.md b/skills/penulis-ilmiah/NOTICE.md new file mode 100644 index 0000000..7533d15 --- /dev/null +++ b/skills/penulis-ilmiah/NOTICE.md @@ -0,0 +1,3 @@ +# Notice: penulis-ilmiah + +First-party OpenCodeHighEnd skill and MCP server for Indonesian academic writing, EYD V spelling, KBBI standard words, and fail-closed citation verification under the MIT License. diff --git a/skills/penulis-ilmiah/SKILL.md b/skills/penulis-ilmiah/SKILL.md new file mode 100644 index 0000000..f5e7f8a --- /dev/null +++ b/skills/penulis-ilmiah/SKILL.md @@ -0,0 +1,91 @@ +--- +name: penulis-ilmiah +description: Menulis dan memeriksa teks ilmiah Bahasa Indonesia (EYD V, kata baku KBBI) untuk esai, skripsi, jurnal SINTA, abstrak Scopus, dan proposal lomba; mencari serta memverifikasi rujukan (anti DOI/halu). Gunakan saat butuh draf ilmiah ID yang lolos ejaan/baku dan daftar pustaka terverifikasi. Bukan untuk Turnitin evasion, bukan pengganti skill academic English-first peer review penuh, bukan OCR (smartdoc). +compatibility: opencode +license: MIT +metadata: + lang: id + mcp: penulis-ilmiah +--- + +# Penulis Ilmiah + +Spesialis penulisan dan penyuntingan naskah ilmiah berbahasa Indonesia berstandar EYD V dan kata baku KBBI dengan verifikasi rujukan mutlak (*fail-closed* / anti-halu sitasi). + +--- + +## Kapan Dipakai + +- Menyusun atau menyunting esai ilmiah, bab skripsi, artikel jurnal nasional (SINTA), dan proposal lomba ilmiah/kreatif berbahasa Indonesia. +- Menyusun abstrak berbahasa Inggris untuk target indeksasi Scopus **berdasarkan klaim faktual yang telah terverifikasi sumbernya**. +- Memeriksa ejaan kata depan, imbuhan, tanda baca, serta kebakuan kata Bahasa Indonesia. +- Mencari dan memverifikasi rujukan ilmiah ke Crossref, OpenAlex, atau Semantic Scholar secara sahih. + +--- + +## Batas Tanggung Jawab + +| Kebutuhan Pengguna | Rute Spesialis | +|---|---| +| Telaah metodologi, sintesis literatur global, manuskrip English-first IMRaD, dan telaah sejawat (*peer critique*) | `academic` | +| Pembersihan klise atau gaya teks AI pada prosa umum non-ilmiah | `humanizer` / `/unslop` | +| Ingest berkas (PDF/DOCX), ekstraksi tabel, OCR, dan rendering layout PDF/DOCX | `smartdoc` | +| Pengumpulan data web langsung atau dokumentasi API resmi | `research` | +| Penulisan ilmiah Bahasa Indonesia (EYD V, kata baku KBBI, anti-halu rujukan) | **`penulis-ilmiah`** | + +--- + +## Alur Kerja (Workflow) + +1. **Penentuan Format & Muat Panduan Gaya**: + - Tentukan jenis tulisan (skripsi, esai, SINTA, proposal lomba, atau Scopus) dan rujuk panduan pada [references/gaya_tulisan.md](references/gaya_tulisan.md). +2. **Penyusunan Kerangka & Draf**: + - Tulis draf secara lugas, objektif, dan terstruktur. Hindari frasa bergaya AI yang tercantum pada [references/frasa_terlarang.md](references/frasa_terlarang.md). +3. **Pemeriksaan Ejaan & Kebakuan Kata**: + - Panggil tool MCP `cek_ejaan` untuk mendeteksi penulisan kata depan menempel (*di, ke, dari*), spasi sebelum tanda baca, dan elipsis sesuai kaidah [references/eyd/](references/eyd/). + - Panggil tool MCP `cek_baku` untuk memeriksa kata tidak baku terhadap daftar [references/kata_baku.md](references/kata_baku.md). +4. **Verifikasi Rujukan (Fail-Closed)**: + - Setiap klaim faktual, angka statistik, atau kutipan harus memiliki rujukan kandidat. + - Panggil tool MCP `verifikasi_rujukan` untuk setiap rujukan kandidat. + - **Hanya rujukan berstatus `VALID` yang diizinkan masuk ke dalam teks dokumen dan daftar pustaka.** + - Rujukan berstatus `TIDAK_COCOK` atau `TIDAK_DITEMUKAN` wajib dikeluarkan dari naskah dan dimasukkan ke dalam laporan rujukan yang ditolak. +5. **Pemformatan Sitasi**: + - Panggil tool MCP `format_sitasi` dengan gaya `apa7` atau `ieee` sesuai [references/gaya_sitasi.md](references/gaya_sitasi.md). Tool menolak memformat data yang belum lolos verifikasi (`wajib_terverifikasi=true`). +6. **Penyajian Luaran**: + - Sajikan dokumen, daftar pustaka, dan laporan pemeriksaan secara terpisah. + +--- + +## Aturan Keras (Hard Rules) + +1. **Anti-Halu Rujukan Mutlak**: + - Dilarang keras mengarang DOI, judul artikel, nama pengarang, tahun penerbitan, volume, halaman, atau angka data statistik. +2. **Fail-Closed Sitasi**: + - Rujukan yang gagal verifikasi (`TIDAK_COCOK` atau `TIDAK_DITEMUKAN`) **tidak boleh** dimasukkan ke dalam daftar pustaka akhir dokumen. Cantumkan rujukan tersebut pada seksi laporan penolakan. +3. **Klaim Tanpa Sumber Wajib Ditolak atau Ditandai**: + - Jika suatu klaim data atau kutipan tidak memiliki rujukan yang valid, hapus klaim tersebut atau tandai secara eksplisit dengan `[UNVERIFIED]` untuk diverifikasi oleh pengguna. +4. **Kebijakan KBBI On-Demand**: + - Tidak melakukan dump kamus KBBI massal ke penyimpanan permanen. Lookup dilakukan per kata dan disimpan dalam cache lokal sementara. +5. **Tanpa Evasion Turnitin**: + - Modul ini tidak menyediakan jalan pintas untuk memanipulasi deteksi plagiarisme atau skor Turnitin. Integritas akademis adalah prioritas utama. +6. **Status Server MCP**: + - Jika server MCP `penulis-ilmiah` tidak terdaftar atau tidak aktif, laporkan status `NOT_CONFIGURED`. Jangan berpura-pura telah melakukan verifikasi eksternal. + +--- + +## Format Luaran Standar + +```markdown +## Dokumen +[Naskah ilmiah hasil penulisan atau penyuntingan yang bersih dari frasa klise AI dan mematuhi kaidah EYD V] + +## Daftar Pustaka +[Daftar rujukan berformat APA 7th atau IEEE yang hanya memuat sumber dengan status VALID] + +## Laporan Pemeriksaan +- **Temuan EYD V**: [Daftar perbaikan ejaan dan tanda baca] +- **Temuan Kata Baku**: [Daftar konversi kata baku KBBI] +- **Rujukan Terverifikasi (VALID)**: [Daftar DOI dan judul yang cocok] +- **Rujukan Ditolak (NON-VALID)**: [Daftar rujukan fiktif/tidak cocok yang disingkirkan] +- **Perlu Cek Manusia**: [Tanda [PERLU_CEK_MANUSIA] untuk konteks khusus] +``` diff --git a/skills/penulis-ilmiah/references/README.md b/skills/penulis-ilmiah/references/README.md new file mode 100644 index 0000000..9d000d1 --- /dev/null +++ b/skills/penulis-ilmiah/references/README.md @@ -0,0 +1,11 @@ +# Indeks Referensi Penulis Ilmiah + +Panduan pengetahuan, ejaan, kata baku, dan sitasi untuk skill `penulis-ilmiah`: + +| Berkas / Direktori | Deskripsi | +|---|---| +| [eyd/](eyd/README.md) | Ringkasan operasional EYD V (Huruf Kapital, Miring, Tebal, Kata Depan, Imbuhan, Partikel, Singkatan/Akronim, Angka/Bilangan, Tanda Baca, Unsur Serapan) | +| [kata_baku.md](kata_baku.md) | Daftar pasangan kata baku KBBI (≥100 entri) beserta flag `[PERLU_CEK_MANUSIA]` | +| [gaya_tulisan.md](gaya_tulisan.md) | Panduan struktur dan gaya penulisan esai ilmiah, bab skripsi, artikel SINTA, proposal lomba, dan abstrak Scopus | +| [gaya_sitasi.md](gaya_sitasi.md) | Aturan format sitasi daftar pustaka dan sitasi dalam teks (*in-text*) untuk format APA 7th dan IEEE | +| [frasa_terlarang.md](frasa_terlarang.md) | Daftar kata klise, transisi berlebihan, dan frasa bergaya AI yang dilarang dalam penulisan ilmiah formal | diff --git a/skills/penulis-ilmiah/references/eyd/01_huruf_kapital.md b/skills/penulis-ilmiah/references/eyd/01_huruf_kapital.md new file mode 100644 index 0000000..606a4b3 --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/01_huruf_kapital.md @@ -0,0 +1,29 @@ +# Huruf kapital + +Sumber: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-kapital/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-kapital/) + +## Aturan yang sering dipakai di tulisan ilmiah + +| ID | Ringkas | Contoh benar | Contoh salah | URL | +|----|---------|--------------|--------------|-----| +| KAP.01 | Huruf pertama awal kalimat kapital | Penelitian ini bertujuan … | penelitian ini bertujuan … (awal kalimat) | https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-kapital/ | +| KAP.02 | Unsur nama orang kapital | Mohammad Hatta | mohammad hatta | sama | +| KAP.03 | Nama jenis/satuan dari nama orang **tidak** kapital | 15 watt; ikan mujair | 15 Watt (sebagai satuan) | sama | +| KAP.04 | Teori/hukum/rumus: nama diri kapital | teori Darwin; hukum Archimedes | teori darwin | sama | +| KAP.06 | Awal petikan langsung kapital | Ibu berkata, "Berhati-hatilah!" | Ibu berkata, "berhati-hatilah!" | sama | +| KAP.08 | Gelar diikuti nama: unsur gelar kapital | Doktor Mohammad Hatta | doktor Mohammad Hatta (sebagai gelar resmi diikuti nama) | sama | +| KAP.10 | Jabatan + nama / pengganti nama kapital | Gubernur Riau; Presiden Republik Indonesia | gubernur riau (sebagai jabatan resmi + wilayah) | sama | +| KAP.11 | Nama bangsa, suku, bahasa, aksara | bahasa Indonesia; suku Dani | Bahasa indonesia (salah pola) | sama | +| KAP.13 | Nama tahun, bulan, hari, hari raya | hari Jumat; bulan Agustus | hari jumat | sama | +| KAP.16 | Nama geografi | Pulau Miangas; Sungai Mahakam | pulau miangas | sama | +| KAP.17 | Unsur geografi tanpa nama diri: nonkapital | berlayar ke teluk | berlayar ke Teluk (umum) | sama | +| KAP.20 | Nama negara/lembaga/dokumen: tiap kata kapital kecuali kata tugas | Undang-Undang Dasar Negara …; Perserikatan Bangsa-Bangsa | undang-undang dasar … (sebagai judul resmi) | sama | +| KAP.21 | Judul buku/artikel/makalah: tiap kata kapital kecuali kata tugas tak di awal | Penerapan Asas-Asas Hukum Perdata | penerapan asas-asas hukum perdata (sebagai judul) | sama | +| KAP.22 | Singkatan gelar/pangkat: kapital sesuai pedoman | M.Si.; S.E.; Kol. | m.si | sama | +| KAP.23 | Sapaan kekerabatan | "Kapan Bapak berangkat?" | "Kapan bapak berangkat?" (sapaan) | sama | +| KAP.Anda | Kata **Anda** kapital | Sudahkah Anda membaca …? | Sudahkah anda …? | sama | + +## Catatan agent + +- Di tubuh paragraf biasa, jabatan tanpa nama sering nonkapital: *Dia menjadi gubernur pada 2020.* Bedakan dari sapaan/jabatan resmi + nama. `[PERLU_CEK_MANUSIA]` untuk kasus abu-abu. +- Jangan kapitalisasi berlebihan gaya Inggris di judul Indonesia yang bukan judul formal. diff --git a/skills/penulis-ilmiah/references/eyd/02_huruf_miring.md b/skills/penulis-ilmiah/references/eyd/02_huruf_miring.md new file mode 100644 index 0000000..7cb61cb --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/02_huruf_miring.md @@ -0,0 +1,15 @@ +# Huruf miring + +Sumber: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-miring/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-miring/) + +| ID | Ringkas | Contoh | URL | +|----|---------|--------|-----| +| MIR.01 | Menegaskan / mengkhususkan kata atau bagian kata | Bentuk *ter-* pada *terlihat* … | https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-miring/ | +| MIR.02 | Judul buku, majalah, surat kabar (dalam teks latinan) | novel *Laskar Pelangi* | sama | +| MIR.03 | Kata/istilah asing yang belum diserap penuh | *feedback*, *default* (jika belum dibakukan dalam konteks) | sama | +| MIR.04 | Nama ilmiah Latin | *Homo sapiens*, *Oryza sativa* | sama | + +## Catatan + +- Istilah asing yang sudah menjadi unsur serapan baku biasanya **tidak** dimiringkan (lihat `11_unsur_serapan.md`). `[PERLU_CEK_MANUSIA]` +- Di Markdown, huruf miring = `*teks*` atau `_teks_`. diff --git a/skills/penulis-ilmiah/references/eyd/03_huruf_tebal.md b/skills/penulis-ilmiah/references/eyd/03_huruf_tebal.md new file mode 100644 index 0000000..f2d8d51 --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/03_huruf_tebal.md @@ -0,0 +1,12 @@ +# Huruf tebal + +Sumber: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-tebal/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-tebal/) + +| ID | Ringkas | Catatan | URL | +|----|---------|---------|-----| +| TEB.01 | Menegaskan bagian tulisan yang sudah ditulis miring | jarang di tubuh skripsi; lebih ke tipografi khusus | https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-tebal/ | +| TEB.02 | Penegasan pada tajuk/bab sesuai kebijakan penerbit | ikuti template kampus/jurnal | sama | + +## Catatan agent + +Untuk skripsi/jurnal, jangan menebalkan kata secara acak. Tebal biasanya untuk heading, bukan penekanan emosional. diff --git a/skills/penulis-ilmiah/references/eyd/04_kata_depan.md b/skills/penulis-ilmiah/references/eyd/04_kata_depan.md new file mode 100644 index 0000000..06bbc4f --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/04_kata_depan.md @@ -0,0 +1,27 @@ +# Kata depan + +Sumber: [https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-depan/](https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-depan/) + +## Aturan inti + +**Kata depan `di`, `ke`, dan `dari` ditulis terpisah dari kata yang mengikutinya.** + +| Benar | Salah | Catatan | +|-------|-------|---------| +| di kantor | dikantor | kata depan tempat | +| ke sekolah | kesekolah | | +| dari rumah | darirumah | | +| di dalam | didalam | | +| ke luar kota | keluar kota (makna berbeda) | *keluar* = verba; *ke luar* = kata depan + *luar* | + +## Bedakan dengan imbuhan `di-` (pasif) + +| Bentuk | Jenis | Contoh | +|--------|-------|--------| +| ditulis | imbuhan pasif `di-` | Naskah itu **ditulis** ulang. | +| di tulis (salah jika maksud pasif) | — | jangan pisah imbuhan | +| di meja | kata depan | Buku ada **di meja**. | + +Heuristik agent: jika setelah `di` langsung verba dasar yang membentuk pasif umum (`buat`, `tulis`, `baca`, `lihat`, …) → kemungkinan imbuhan; jika nominal tempat/arah → kata depan terpisah. Ambigu → `[PERLU_CEK_MANUSIA]`. + +URL: https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-depan/ diff --git a/skills/penulis-ilmiah/references/eyd/05_imbuhan_kata_turunan.md b/skills/penulis-ilmiah/references/eyd/05_imbuhan_kata_turunan.md new file mode 100644 index 0000000..d6f69c2 --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/05_imbuhan_kata_turunan.md @@ -0,0 +1,16 @@ +# Kata turunan / imbuhan + +Sumber: [https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-turunan/](https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-turunan/) + +| ID | Ringkas | Contoh benar | URL | +|----|---------|--------------|-----| +| TUR.01 | Imbuhan dirangkai dengan kata dasar | diperbaiki; menulis; kemajuan | https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-turunan/ | +| TUR.02 | Gabungan kata mendapat imbuhan: perhatikan pola resmi | mempertanggungjawabkan (cek bentuk baku) | sama | +| TUR.03 | Bentuk terikat digabung | antarnegara; pascasarjana; nonformal (cek daftar serapan) | sama | + +## Yang sering salah di skripsi + +- Memisahkan imbuhan: `di tulis`, `ke pada` (salah jika maksud `kepada` sebagai kata depan majemuk baku — *kepada* ditulis serangkai). +- `kepada` / `daripada` ditulis serangkai (bukan kata depan `ke`/`dari` + `pada` terpisah dalam fungsi itu). `[PERLU_CEK_MANUSIA]` untuk kalimat spesifik. + +Lihat juga pemenggalan: https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/pemenggalan-kata/ diff --git a/skills/penulis-ilmiah/references/eyd/06_partikel.md b/skills/penulis-ilmiah/references/eyd/06_partikel.md new file mode 100644 index 0000000..20adfbb --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/06_partikel.md @@ -0,0 +1,11 @@ +# Partikel + +Sumber: [https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/partikel/](https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/partikel/) + +| Partikel | Cara tulis | Contoh | +|----------|------------|--------| +| *pun* | dipisah | Apa **pun** alasannya …; Buku itu **pun** hilang. | +| *per* | dipisah dalam makna ‘mulai’, ‘demi’, ‘tiap’ | **per** 1 September; **per** kilogram | +| *-lah*, *-kah*, *-tah* | dirangkai | bacalah; siapakah | + +Catatan: *walaupun*, *meskipun*, *adapun* ditulis serangkai (bukan *pun* terpisah). URL di atas. diff --git a/skills/penulis-ilmiah/references/eyd/07_singkatan_akronim.md b/skills/penulis-ilmiah/references/eyd/07_singkatan_akronim.md new file mode 100644 index 0000000..eaaf84c --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/07_singkatan_akronim.md @@ -0,0 +1,16 @@ +# Singkatan dan akronim + +Sumber: [https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/singkatan/](https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/singkatan/) + +| Jenis | Pola | Contoh | +|-------|------|--------| +| Singkatan nama orang | huruf awal kapital + titik | A.H. Nasution | +| Singkatan gelar | sesuai daftar resmi | M.Hum.; S.Kom. | +| Singkatan umum berpoin | titik | hlm.; dkk.; yth. | +| Singkatan huruf kapital tanpa titik (umum lembaga) | PBB, ASEAN, UI | ikuti kebiasaan resmi | +| Akronim | biasanya tanpa titik; kapitalisasi sesuai lafaz/konvensi | Unesco / UNESCO tergantung konteks resmi | + +## Tulisan ilmiah + +- Definisikan singkatan di kemunculan pertama: *sistem informasi akademik (SIA)* +- Jangan campur gaya titik/non-titik untuk singkatan yang sama dalam satu dokumen diff --git a/skills/penulis-ilmiah/references/eyd/08_gelar.md b/skills/penulis-ilmiah/references/eyd/08_gelar.md new file mode 100644 index 0000000..d1d7eaa --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/08_gelar.md @@ -0,0 +1,16 @@ +# Gelar + +Gabungan aturan kapital + singkatan. + +Sumber utama: + +- https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-kapital/ +- https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/singkatan/ + +| Pola | Contoh benar | +|------|--------------| +| Gelar + nama | Doktor Ir. Soekarno; Prof. Dr. … | +| Nama + gelar akademik | Ani Wijaya, S.Kom., M.T. | +| Singkatan gelar | S.E.; M.Si.; Dr. (cek daftar EYD) | + +Hindari: `DR.` all-caps nonstandar; `S.Kom` tanpa titik jika pedoman kampus mengikuti EYD bertitik. Beberapa kampus punya *template* sendiri — utamakan template lembaga, catat di laporan jika bentrok EYD. `[PERLU_CEK_MANUSIA]` diff --git a/skills/penulis-ilmiah/references/eyd/09_angka_bilangan.md b/skills/penulis-ilmiah/references/eyd/09_angka_bilangan.md new file mode 100644 index 0000000..76732a3 --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/09_angka_bilangan.md @@ -0,0 +1,12 @@ +# Angka dan bilangan + +Sumber: [https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/angka-dan-bilangan/](https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/angka-dan-bilangan/) + +| ID | Ringkas | Contoh | +|----|---------|--------| +| ANG.01 | Bilangan utuh yang dapat dinyatakan dengan satu atau dua kata biasanya ditulis dengan huruf di tubuh teks formal | dua puluh mahasiswa | +| ANG.02 | Pakai angka untuk bilangan panjang, statistik, tanggal, nomor | 125 responden; 12 Oktober 2026 | +| ANG.03 | Persentase & satuan: konsisten | 25%; 3 cm | +| ANG.04 | Jangan mulai kalimat dengan angka Arab — eja atau susun ulang | Dua puluh lima persen … / Sebanyak 25% … | + +Catatan jurnal: banyak template SINTA/Scopus mewajibkan angka untuk measurement — ikuti template, sebutkan di laporan jika menyimpang dari preferensi EYD naratif. diff --git a/skills/penulis-ilmiah/references/eyd/10_tanda_baca.md b/skills/penulis-ilmiah/references/eyd/10_tanda_baca.md new file mode 100644 index 0000000..685653f --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/10_tanda_baca.md @@ -0,0 +1,68 @@ +# Tanda baca (15) +Indeks resmi: https://ejaan.kemendikdasmen.go.id/ + +## Tanda titik +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-titik/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-titik/) +- Ringkas: Akhiri kalimat deklaratif; pemisah ribuan dalam angka (kebiasaan ID); singkatan. + +## Tanda koma +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-koma/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-koma/) +- Ringkas: Pemisah unsur dalam pemerincian; petikan; anak kalimat; desimal ID (opsional tergantung gaya). + +## Tanda titik koma +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-titik-koma/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-titik-koma/) +- Ringkas: Pemisah bagian kalimat setara yang sudah memakai koma. + +## Tanda titik dua +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-titik-dua/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-titik-dua/) +- Ringkas: Memperkenalkan pemerincian atau kutipan; jam. + +## Tanda hubung +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-hubung/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-hubung/) +- Ringkas: Menghubungkan unsur kata ulang terbagi; pecahan alamat; penghubung. + +## Tanda pisah +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-pisah/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-pisah/) +- Ringkas: Membatasi sisipan atau menunjukkan rentang (beda dari hubung). + +## Tanda tanya +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-tanya/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-tanya/) +- Ringkas: Akhir kalimat tanya. + +## Tanda seru +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-seru/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-seru/) +- Ringkas: Intonasi seru — jarang di teks ilmiah; hindari berlebihan. + +## Tanda elipsis +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-elipsis/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-elipsis/) +- Ringkas: Menandai bagian yang dihilangkan dalam kutipan (… ), bukan tiga titik asal. + +## Tanda petik +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-petik/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-petik/) +- Ringkas: Kutipan langsung; judul artikel dalam tubuh teks sesuai gaya. + +## Tanda petik tunggal +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-petik-tunggal/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-petik-tunggal/) +- Ringkas: Kutipan dalam kutipan; terjemahan makna. + +## Tanda kurung +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-kurung/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-kurung/) +- Ringkas: Keterangan tambahan; rujukan; perluasan. + +## Tanda kurung siku +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-kurung-siku/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-kurung-siku/) +- Ringkas: Sisipan dalam kutipan; keterangan editorial. + +## Tanda garis miring +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-garis-miring/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-garis-miring/) +- Ringkas: Pengganti *atau*; pecahan; pemisah data tertentu. + +## Tanda apostrof +- URL: [https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-apostrof/](https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-tanda-baca/tanda-apostrof/) +- Ringkas: Menandai penghilangan bunyi; bukan untuk jamak gaya Inggris. + +## Catatan ilmiah + +- Hindari tanda seru. +- Kutipan panjang: ikuti gaya kampus (blok vs petik). +- Elipsis resmi EYD: … (bukan `...` bermakna sama di beberapa template — konsistenkan). `[PERLU_CEK_MANUSIA]` diff --git a/skills/penulis-ilmiah/references/eyd/11_unsur_serapan.md b/skills/penulis-ilmiah/references/eyd/11_unsur_serapan.md new file mode 100644 index 0000000..1b117d9 --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/11_unsur_serapan.md @@ -0,0 +1,14 @@ +# Unsur serapan + +Sumber: + +- Umum: https://ejaan.kemendikdasmen.go.id/eyd/penulisan-unsur-serapan/serapan-umum/ +- Khusus: https://ejaan.kemendikdasmen.go.id/eyd/penulisan-unsur-serapan/serapan-khusus/ + +| Prinsip | Contoh arah | +|---------|-------------| +| Sesuaikan ejaan serapan yang sudah dibakukan | *system* → **sistem**; *analysis* → **analisis**; *method* → **metode** | +| Istilah belum baku / perlu penekanan: miringkan | *blockchain* (jika belum distabilkan di bidang Anda) | +| Jangan campur ejaan asing dan serapan untuk istilah yang sama dalam satu dokumen | pilih *standard* ATAU *standar* secara konsisten sesuai KBBI | + +Daftar pasangan sering salah → lihat `../kata_baku.md`. diff --git a/skills/penulis-ilmiah/references/eyd/README.md b/skills/penulis-ilmiah/references/eyd/README.md new file mode 100644 index 0000000..daf5cc2 --- /dev/null +++ b/skills/penulis-ilmiah/references/eyd/README.md @@ -0,0 +1,21 @@ +# Ringkasan EYD V (operasional) + +Sumber resmi: [https://ejaan.kemendikdasmen.go.id/](https://ejaan.kemendikdasmen.go.id/) + +Setiap file di folder ini adalah **ringkasan untuk agent**, bukan salinan utuh pedoman. Tiap aturan punya tautan halaman resmi. Tandai `[PERLU_CEK_MANUSIA]` jika ragu. + +Indeks: + +| File | Topik | URL utama | +|------|-------|-----------| +| `01_huruf_kapital.md` | Huruf kapital | https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-kapital/ | +| `02_huruf_miring.md` | Huruf miring | https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-miring/ | +| `03_huruf_tebal.md` | Huruf tebal | https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-tebal/ | +| `04_kata_depan.md` | Kata depan | https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-depan/ | +| `05_imbuhan_kata_turunan.md` | Kata turunan / imbuhan | https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/kata-turunan/ | +| `06_partikel.md` | Partikel | https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/partikel/ | +| `07_singkatan_akronim.md` | Singkatan | https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/singkatan/ | +| `08_gelar.md` | Gelar (bagian kapital + singkatan) | https://ejaan.kemendikdasmen.go.id/eyd/penggunaan-huruf/huruf-kapital/ | +| `09_angka_bilangan.md` | Angka dan bilangan | https://ejaan.kemendikdasmen.go.id/eyd/penulisan-kata/angka-dan-bilangan/ | +| `10_tanda_baca.md` | 15 tanda baca | https://ejaan.kemendikdasmen.go.id/eyd/ (bagian Penggunaan Tanda Baca) | +| `11_unsur_serapan.md` | Unsur serapan | https://ejaan.kemendikdasmen.go.id/eyd/penulisan-unsur-serapan/serapan-umum/ | diff --git a/skills/penulis-ilmiah/references/frasa_terlarang.md b/skills/penulis-ilmiah/references/frasa_terlarang.md new file mode 100644 index 0000000..9a09342 --- /dev/null +++ b/skills/penulis-ilmiah/references/frasa_terlarang.md @@ -0,0 +1,39 @@ +# Frasa gaya AI yang harus dihindari + +Jangan pakai (atau ganti dengan kalimat konkret): + +| Hindari | Arah perbaikan | +|---------|----------------| +| Dalam era digital yang semakin berkembang | Sebut fenomena konkret + sumber | +| Menarik benang merah | Ringkas temuan tanpa klise | +| Perlu dicatat bahwa | Langsung ke pernyataan | +| Sangat penting untuk diingat | Hapus atau buat spesifik | +| Secara keseluruhan dapat disimpulkan bahwa | *Simpulannya:* + isi | +| Merupakan suatu | *adalah* / hapus *suatu* | +| Dengan demikian maka | Pilih satu: *dengan demikian* ATAU *maka* | +| Tidak dapat dipungkiri | Langsung klaim + bukti | +| Sebuah perjalanan panjang | Hapus metafora kosong | +| Membuka wawasan baru | Sebut wawasan apa | +| Landasan kokoh untuk penelitian selanjutnya | Sebut agenda riset konkret | +| Paradigma baru yang revolusioner | Kurangi hiperbola | +| Delve into / dive deep (campur EN) | *membahas*, *menganalisis* | +| It's worth noting that | Hapus | +| In conclusion, | *Simpulan:* | +| Leveraging synergies | Bahasa konkret | +| Robust framework | Jelaskan isi kerangka | +| Cutting-edge | Sebut teknologi/versi | +| Landscape (metafora berlebih) | *kondisi*, *peta masalah* | +| Underscores the importance | *menunjukkan* + data | +| Multifaceted | Sebut aspeknya | +| Holistic approach | Uraikan langkah | +| Game changer | Hindari | +| At the end of the day | Hapus | +| Moving forward | *ke depan* + rencana | +| Utilize (jika *use* cukup) | *menggunakan* | +| Furthermore, Moreover, beruntun tiap kalimat | Variasikan struktur | +| Pertama-tama yang perlu digarisbawahi | Langsung poin | +| Dari segi perspektif | *dari perspektif* / *dari segi* | +| Dilakukanlah | *dilakukan* | +| Adalah merupakan | Pilih satu | + +Tambahan dari skill `humanizer` / anti-slop boleh digabung saat menyunting prosa. diff --git a/skills/penulis-ilmiah/references/gaya_sitasi.md b/skills/penulis-ilmiah/references/gaya_sitasi.md new file mode 100644 index 0000000..78c7b8b --- /dev/null +++ b/skills/penulis-ilmiah/references/gaya_sitasi.md @@ -0,0 +1,33 @@ +# Gaya sitasi APA 7 dan IEEE (ringkas) + +Pakai tool `format_sitasi` (CSL) sebagai sumber kebenaran format. Ringkasan di bawah hanya panduan agent. + +## APA 7 — in-text + +- Satu penulis: (LeCun, 2015) atau LeCun (2015) +- Dua penulis: (Akiba & Sano, 2019) +- Tiga atau lebih: (Akiba et al., 2019) sejak kutipan pertama (APA 7) +- Kutipan langsung: sertakan halaman jika ada (hanya jika halaman VALID dari sumber) + +## APA 7 — daftar pustaka (artikel jurnal) + +Pola umum: + +> Penulis, A. A., & Penulis, B. B. (Tahun). Judul artikel. *Nama Jurnal*, *volume*(issue), halaman. https://doi.org/xx.xxxx/xxxxx + +## IEEE — in-text + +- Nomor kurung siku berurutan: [1], [2], [3]–[5] +- Urutan daftar = urutan kemunculan, bukan abjad + +## IEEE — daftar pustaka (artikel) + +Pola umum: + +> [1] A. Author and B. Author, “Title of article,” *Abbrev. Periodical*, vol. x, no. x, pp. xx–xx, Mon. Year, doi: xx.xxxx/xxxxx. + +## Aturan paket ini + +1. Jangan menghasilkan entri pustaka tanpa metadata VALID. +2. DOI ditulis konsisten (`https://doi.org/...` atau `doi:` sesuai gaya). +3. Jika metadata kurang (tanpa volume), jangan mengarang angka — biarkan field kosong / minta pengguna. diff --git a/skills/penulis-ilmiah/references/gaya_tulisan.md b/skills/penulis-ilmiah/references/gaya_tulisan.md new file mode 100644 index 0000000..ab8c3ad --- /dev/null +++ b/skills/penulis-ilmiah/references/gaya_tulisan.md @@ -0,0 +1,79 @@ +# Panduan gaya per jenis tulisan + +## 1. Esai ilmiah (ID) + +1. Pendahuluan (latar + tesis) +2. Isi berparagraf (satu gagasan utama per paragraf) +3. Penutup (simpulan + implikasi singkat) +4. Daftar pustaka (APA 7 default) + +Hindari: absolut tanpa data (*selalu*, *semua orang*); frasa AI (`frasa_terlarang.md`). + +## 2. Skripsi / tugas akhir (struktur umum Indonesia) + +Urutan tipikal (sesuaikan template kampus — **template kampus menang**): + +1. Halaman judul, pengesahan, pernyataan orisinalitas +2. Abstrak ID + Abstract EN +3. Kata kunci +4. Prakata / ucapan terima kasih +5. Daftar isi, tabel, gambar +6. Bab 1 Pendahuluan (latar, rumusan, tujuan, manfaat, ruang lingkup) +7. Bab 2 Tinjauan pustaka / landasan teori +8. Bab 3 Metode penelitian +9. Bab 4 Hasil dan pembahasan +10. Bab 5 Simpulan dan saran +11. Daftar pustaka +12. Lampiran + +Sitasi: sering APA 7; beberapa teknik pakai IEEE. `[PERLU_CEK_MANUSIA]` — baca buku pedoman kampus. + +## 3. Jurnal SINTA (ID atau campur) + +- Ikuti **author guideline** jurnal target (template menang). +- Struktur umum: Judul → Identitas → Abstrak → Kata kunci → Pendahuluan → Metode → Hasil → Pembahasan → Simpulan → Ucapan terima kasih → Daftar pustaka. +- Banyak jurnal Indonesia memakai IMRaD. +- Cantumkan ORCID jika diminta. +- Rujukan: utamakan jurnal primer; semua DOI diverifikasi. + +## 4. Jurnal Scopus (Inggris) + +- Bahasa tubuh: English academic; ejaan US/UK **konsisten**. +- IMRaD ketat; highlight novelty & contribution di Introduction. +- Abstrak: tujuan, metode, hasil utama (angka hanya jika tervalidasi sumber/data Anda), simpulan. +- Sitasi: APA atau IEEE / Elsevier Harvard — ikuti journal. +- Jangan klaim “first study ever” tanpa bukti VALID. + +## 5. Abstrak + +Panjang tipikal 150–250 kata (cek template). Isi: + +1. Konteks singkat +2. Tujuan +3. Metode +4. Temuan utama (tanpa rujukan biasanya) +5. Implikasi/simpulan + +Hindari sitasi di abstrak kecuali journal mengizinkan. + +## 6. Kata kunci + +- 3–5 kata/frasa +- Bukan pengulangan judul utuh +- Urut abjad atau urgensi — ikuti journal +- Pakai istilah yang searchable + +## 7. Proposal lomba + +Struktur tipikal: + +1. Judul +2. Latar belakang & masalah +3. Tujuan & manfaat +4. Gagasan / metode / tahapan +5. Luaran +6. Jadwal +7. Anggaran (jika diminta) +8. Daftar pustaka **wajib VALID** + +Dosen sering menolak proposal dengan DOI palsu — jalankan `verifikasi_rujukan` untuk setiap item. diff --git a/skills/penulis-ilmiah/references/kata_baku.md b/skills/penulis-ilmiah/references/kata_baku.md new file mode 100644 index 0000000..0146882 --- /dev/null +++ b/skills/penulis-ilmiah/references/kata_baku.md @@ -0,0 +1,262 @@ +# Daftar kata baku / tidak baku (awal) + +**Hak cipta KBBI:** jangan menyalin definisi massal. Hanya pasangan bentuk + URL cek per kata. + +- Portal cepat (non-API resmi): https://kbbi.web.id/ +- KBBI resmi: https://kbbi.kemendikdasmen.go.id/ + +Kolom `cek_manusia`: `ya` berarti agent/manusia harus membuka KBBI sebelum mengunci aturan. + +| # | Tidak baku / diragukan | Baku / saran | URL cek (bentuk saran) | Catatan | cek_manusia | +|---|------------------------|--------------|------------------------|---------|-------------| +| 1 | aktip | aktif | https://kbbi.web.id/aktif | umum | disarankan_cek | +| 2 | aktipitas | aktivitas | https://kbbi.web.id/aktivitas | umum | disarankan_cek | +| 3 | aktifitas | aktivitas | https://kbbi.web.id/aktivitas | umum | disarankan_cek | +| 4 | analisa | analisis | https://kbbi.web.id/analisis | umum ilmiah | disarankan_cek | +| 5 | analisa data | analisis data | https://kbbi.web.id/analisis | frasa | disarankan_cek | +| 6 | apotik | apotek | https://kbbi.web.id/apotek | umum | disarankan_cek | +| 7 | azas | asas | https://kbbi.web.id/asas | umum | disarankan_cek | +| 8 | azasi | asasi | https://kbbi.web.id/asasi | umum | disarankan_cek | +| 9 | ijin | izin | https://kbbi.web.id/izin | umum | disarankan_cek | +| 10 | diijinkan | diizinkan | https://kbbi.web.id/diizinkan | umum | disarankan_cek | +| 11 | hakekat | hakikat | https://kbbi.web.id/hakikat | umum | disarankan_cek | +| 12 | praktek | praktik | https://kbbi.web.id/praktik | umum | disarankan_cek | +| 13 | tehnik | teknik | https://kbbi.web.id/teknik | umum | disarankan_cek | +| 14 | tehnologi | teknologi | https://kbbi.web.id/teknologi | umum | disarankan_cek | +| 15 | metoda | metode | https://kbbi.web.id/metode | umum ilmiah | disarankan_cek | +| 16 | methode | metode | https://kbbi.web.id/metode | campur EN | disarankan_cek | +| 17 | obyek | objek | https://kbbi.web.id/objek | umum | disarankan_cek | +| 18 | obyektif | objektif | https://kbbi.web.id/objektif | umum | disarankan_cek | +| 19 | subyek | subjek | https://kbbi.web.id/subjek | umum | disarankan_cek | +| 20 | subyektif | subjektif | https://kbbi.web.id/subjektif | umum | disarankan_cek | +| 21 | produktip | produktif | https://kbbi.web.id/produktif | umum | disarankan_cek | +| 22 | efektip | efektif | https://kbbi.web.id/efektif | umum | disarankan_cek | +| 23 | efektifitas | efektivitas | https://kbbi.web.id/efektivitas | umum | disarankan_cek | +| 24 | kwalitas | kualitas | https://kbbi.web.id/kualitas | umum | disarankan_cek | +| 25 | kwantitas | kuantitas | https://kbbi.web.id/kuantitas | umum | disarankan_cek | +| 26 | kwitansi | kuitansi | https://kbbi.web.id/kuitansi | umum | ya | +| 27 | resiko | risiko | https://kbbi.web.id/risiko | umum | disarankan_cek | +| 28 | resiko gagal | risiko gagal | https://kbbi.web.id/risiko | frasa | disarankan_cek | +| 29 | sistematisir | sistematisasi | https://kbbi.web.id/sistematisasi | jarang; cek konteks | ya | +| 30 | standard | standar | https://kbbi.web.id/standar | kecuali nama EN | disarankan_cek | +| 31 | standart | standar | https://kbbi.web.id/standar | umum | disarankan_cek | +| 32 | survey | survei | https://kbbi.web.id/survei | umum | disarankan_cek | +| 33 | surveyer | surveyor | https://kbbi.web.id/surveyor | cek KBBI | ya | +| 34 | jadual | jadwal | https://kbbi.web.id/jadwal | umum | disarankan_cek | +| 35 | jadual kuliah | jadwal kuliah | https://kbbi.web.id/jadwal | frasa | disarankan_cek | +| 36 | jempol | ibujari/jempol | https://kbbi.web.id/ibujari | jempol ada di KBBI; cek makna | ya | +| 37 | jornal | jurnal | https://kbbi.web.id/jurnal | typo umum | disarankan_cek | +| 38 | jurnalistik | jurnalistik | https://kbbi.web.id/jurnalistik | sudah baku — jangan ubah | ya | +| 39 | karir | karier | https://kbbi.web.id/karier | umum | disarankan_cek | +| 40 | karier | karier | https://kbbi.web.id/karier | baku | disarankan_cek | +| 41 | ketrampilan | keterampilan | https://kbbi.web.id/keterampilan | umum | disarankan_cek | +| 42 | keseimbangan | keseimbangan | https://kbbi.web.id/keseimbangan | baku | disarankan_cek | +| 43 | komplek | kompleks | https://kbbi.web.id/kompleks | makna 'kawasan/rumit' | disarankan_cek | +| 44 | kompleksitas | kompleksitas | https://kbbi.web.id/kompleksitas | baku | disarankan_cek | +| 45 | kongkrit | konkret | https://kbbi.web.id/konkret | umum | disarankan_cek | +| 46 | konkrit | konkret | https://kbbi.web.id/konkret | umum | disarankan_cek | +| 47 | konsekwensi | konsekuensi | https://kbbi.web.id/konsekuensi | umum | disarankan_cek | +| 48 | kwalitatif | kualitatif | https://kbbi.web.id/kualitatif | umum | disarankan_cek | +| 49 | kwantitatif | kuantitatif | https://kbbi.web.id/kuantitatif | umum | disarankan_cek | +| 50 | mahluk | makhluk | https://kbbi.web.id/makhluk | umum | disarankan_cek | +| 51 | maksimalisir | maksimalkan/maksimisasi | https://kbbi.web.id/maksimalkan | cek konteks | ya | +| 52 | managemen | manajemen | https://kbbi.web.id/manajemen | umum | disarankan_cek | +| 53 | management | manajemen | https://kbbi.web.id/manajemen | kecuali judul EN | disarankan_cek | +| 54 | mesjid | masjid | https://kbbi.web.id/masjid | umum | disarankan_cek | +| 55 | metoda penelitian | metode penelitian | https://kbbi.web.id/metode | frasa ilmiah | disarankan_cek | +| 56 | nomer | nomor | https://kbbi.web.id/nomor | umum | disarankan_cek | +| 57 | nopember | November | https://kbbi.web.id/november | nama bulan | disarankan_cek | +| 58 | oktober | Oktober | https://kbbi.web.id/oktober | nama bulan kapital | disarankan_cek | +| 59 | orpunk | roke/orkes punk? | https://kbbi.web.id/roke | jangan tebak | ya | +| 60 | paska | pasca | https://kbbi.web.id/pasca | bentuk terikat | disarankan_cek | +| 61 | pascasarjana | pascasarjana | https://kbbi.web.id/pascasarjana | baku serangkai | disarankan_cek | +| 62 | pasca sarjana | pascasarjana | https://kbbi.web.id/pascasarjana | pemisahan salah | disarankan_cek | +| 63 | pemirsa | pemirsa | https://kbbi.web.id/pemirsa | baku TV; cek | ya | +| 64 | pentol | pentul | https://kbbi.web.id/pentul | berbeda makna — cek | ya | +| 65 | perijinan | perizinan | https://kbbi.web.id/perizinan | umum | disarankan_cek | +| 66 | persentase | persentase | https://kbbi.web.id/persentase | baku | disarankan_cek | +| 67 | prosentase | persentase | https://kbbi.web.id/persentase | umum | disarankan_cek | +| 68 | propinsi | provinsi | https://kbbi.web.id/provinsi | umum | disarankan_cek | +| 69 | revisi | revisi | https://kbbi.web.id/revisi | baku | disarankan_cek | +| 70 | ritme | ritme | https://kbbi.web.id/ritme | baku | disarankan_cek | +| 71 | ritmee | ritme | https://kbbi.web.id/ritme | typo | disarankan_cek | +| 72 | rileks | relaks | https://kbbi.web.id/relaks | cek KBBI relaks/rileks | ya | +| 73 | sistim | sistem | https://kbbi.web.id/sistem | umum | disarankan_cek | +| 74 | system | sistem | https://kbbi.web.id/sistem | kecuali kode/EN | disarankan_cek | +| 75 | teoritis | teoretis | https://kbbi.web.id/teoretis | umum ilmiah | disarankan_cek | +| 76 | theori | teori | https://kbbi.web.id/teori | umum | disarankan_cek | +| 77 | tradisionil | tradisional | https://kbbi.web.id/tradisional | umum | disarankan_cek | +| 78 | uriKulum | kurikulum | https://kbbi.web.id/kurikulum | typo | disarankan_cek | +| 79 | uri kulum | kurikulum | https://kbbi.web.id/kurikulum | typo | disarankan_cek | +| 80 | variabel | variabel | https://kbbi.web.id/variabel | baku | disarankan_cek | +| 81 | variable | variabel | https://kbbi.web.id/variabel | kecuali EN | disarankan_cek | +| 82 | verifikasi | verifikasi | https://kbbi.web.id/verifikasi | baku | disarankan_cek | +| 83 | frasa | frasa | https://kbbi.web.id/frasa | baku | disarankan_cek | +| 84 | frase | frasa | https://kbbi.web.id/frasa | umum | disarankan_cek | +| 85 | foto copy | fotokopi | https://kbbi.web.id/fotokopi | umum | disarankan_cek | +| 86 | fotocopy | fotokopi | https://kbbi.web.id/fotokopi | umum | disarankan_cek | +| 87 | photo copy | fotokopi | https://kbbi.web.id/fotokopi | umum | disarankan_cek | +| 88 | handphone | ponsel/telepon genggam | https://kbbi.web.id/ponsel | bergantung gaya | ya | +| 89 | hp | HP/ponsel | https://kbbi.web.id/hp | singkatan — konsistenkan | ya | +| 90 | download | unduh | https://kbbi.web.id/unduh | serapan dianjurkan | disarankan_cek | +| 91 | upload | unggah | https://kbbi.web.id/unggah | serapan dianjurkan | disarankan_cek | +| 92 | login | log masuk / masuk log | https://kbbi.web.id/log | atau biarkan istilah TI | ya | +| 93 | file | berkas/file | https://kbbi.web.id/berkas | istilah TI sering dipertahankan | ya | +| 94 | folder | folder/berkas folder | https://kbbi.web.id/folder | istilah TI | ya | +| 95 | website | situs web | https://kbbi.web.id/situs | dianjurkan | disarankan_cek | +| 96 | web site | situs web | https://kbbi.web.id/situs | dianjurkan | disarankan_cek | +| 97 | email | surel/email | https://kbbi.web.id/surel | dua-duanya dipakai; konsisten | ya | +| 98 | e-mail | surel/email | https://kbbi.web.id/surel | konsisten | ya | +| 99 | hobby | hobi | https://kbbi.web.id/hobi | umum | disarankan_cek | +| 100 | hoby | hobi | https://kbbi.web.id/hobi | umum | disarankan_cek | +| 101 | insyaf | insaf | https://kbbi.web.id/insaf | umum | disarankan_cek | +| 102 | islam | Islam | https://kbbi.web.id/islam | nama agama kapital | disarankan_cek | +| 103 | atlit | atlet | https://kbbi.web.id/atlet | umum | disarankan_cek | +| 104 | athlit | atlet | https://kbbi.web.id/atlet | umum | disarankan_cek | +| 105 | cabai | cabai | https://kbbi.web.id/cabai | baku; cabe nonbaku di banyak pedoman | disarankan_cek | +| 106 | cabe | cabai | https://kbbi.web.id/cabai | umum nonbaku | disarankan_cek | +| 107 | cemilan | camilan | https://kbbi.web.id/camilan | umum | disarankan_cek | +| 108 | cendera mata | cenderamata | https://kbbi.web.id/cenderamata | serangkai | disarankan_cek | +| 109 | daerah | daerah | https://kbbi.web.id/daerah | baku | disarankan_cek | +| 110 | dolar | dollar | https://kbbi.web.id/dollar | cek KBBI: dolar | ya | +| 111 | dollar | dolar | https://kbbi.web.id/dolar | KBBI cenderung dolar | ya | +| 112 | eksport | ekspor | https://kbbi.web.id/ekspor | umum | disarankan_cek | +| 113 | import | impor | https://kbbi.web.id/impor | umum | disarankan_cek | +| 114 | ekstrim | ekstrem | https://kbbi.web.id/ekstrem | umum | disarankan_cek | +| 115 | extreem | ekstrem | https://kbbi.web.id/ekstrem | umum | disarankan_cek | +| 116 | frekwensi | frekuensi | https://kbbi.web.id/frekuensi | umum | disarankan_cek | +| 117 | frequency | frekuensi | https://kbbi.web.id/frekuensi | kecuali EN | disarankan_cek | +| 118 | ghaib | gaib | https://kbbi.web.id/gaib | umum | disarankan_cek | +| 119 | ghoib | gaib | https://kbbi.web.id/gaib | umum | disarankan_cek | +| 120 | hadist | hadis | https://kbbi.web.id/hadis | umum | disarankan_cek | +| 121 | hipotesa | hipotesis | https://kbbi.web.id/hipotesis | umum ilmiah | disarankan_cek | +| 122 | hipotesa kerja | hipotesis kerja | https://kbbi.web.id/hipotesis | frasa | disarankan_cek | +| 123 | ideologi | ideologi | https://kbbi.web.id/ideologi | baku | disarankan_cek | +| 124 | idiologi | ideologi | https://kbbi.web.id/ideologi | umum | disarankan_cek | +| 125 | ijazah | ijazah | https://kbbi.web.id/ijazah | baku | disarankan_cek | +| 126 | ijasah | ijazah | https://kbbi.web.id/ijazah | umum | disarankan_cek | +| 127 | intruksi | instruksi | https://kbbi.web.id/instruksi | umum | disarankan_cek | +| 128 | intruction | instruksi | https://kbbi.web.id/instruksi | typo EN | disarankan_cek | +| 129 | jenius | genius | https://kbbi.web.id/genius | cek; jenius juga dipakai | ya | +| 130 | kaedah | kaidah | https://kbbi.web.id/kaidah | umum | disarankan_cek | +| 131 | kaidah | kaidah | https://kbbi.web.id/kaidah | baku | disarankan_cek | +| 132 | kalau | kalau | https://kbbi.web.id/kalau | baku | disarankan_cek | +| 133 | kalo | kalau | https://kbbi.web.id/kalau | lisan | disarankan_cek | +| 134 | kanggo | untuk | https://kbbi.web.id/untuk | nonbaku daerah | ya | +| 135 | kartumember | kartu anggota | https://kbbi.web.id/kartu | campur | ya | +| 136 | kasat mata | kasatmata | https://kbbi.web.id/kasatmata | serangkai sering | ya | +| 137 | katagori | kategori | https://kbbi.web.id/kategori | umum | disarankan_cek | +| 138 | kategory | kategori | https://kbbi.web.id/kategori | umum | disarankan_cek | +| 139 | kebanyakan | kebanyakan | https://kbbi.web.id/kebanyakan | baku | disarankan_cek | +| 140 | kedateng | kedatangan | https://kbbi.web.id/kedatangan | lisan | disarankan_cek | +| 141 | kemaren | kemarin | https://kbbi.web.id/kemarin | umum | disarankan_cek | +| 142 | keset | kesat? | https://kbbi.web.id/kesat | beda makna — jangan otomatis | ya | +| 143 | khalayak | khalayak | https://kbbi.web.id/khalayak | baku | disarankan_cek | +| 144 | khusus | khusus | https://kbbi.web.id/khusus | baku | disarankan_cek | +| 145 | kusus | khusus | https://kbbi.web.id/khusus | umum | disarankan_cek | +| 146 | kwalitet | kualitas | https://kbbi.web.id/kualitas | umum | disarankan_cek | +| 147 | legalisir | legalisasi | https://kbbi.web.id/legalisasi | umum administrasi | disarankan_cek | +| 148 | lemari es | lemari pendingin/kulkas | https://kbbi.web.id/lemari | bergantung gaya | ya | +| 149 | maap | maaf | https://kbbi.web.id/maaf | lisan | disarankan_cek | +| 150 | males | malas | https://kbbi.web.id/malas | lisan | disarankan_cek | +| 151 | memerintahkan | memerintahkan | https://kbbi.web.id/memerintahkan | baku | disarankan_cek | +| 152 | merubah | mengubah | https://kbbi.web.id/mengubah | sangat umum salah | disarankan_cek | +| 153 | dirubah | diubah | https://kbbi.web.id/diubah | umum salah | disarankan_cek | +| 154 | verifikasikan | verifikasi/memverifikasi | https://kbbi.web.id/verifikasi | cek bentuk | ya | +| 155 | nalisa | analisis | https://kbbi.web.id/analisis | typo | disarankan_cek | +| 156 | negri | negeri | https://kbbi.web.id/negeri | umum | disarankan_cek | +| 157 | netizen | warganet | https://kbbi.web.id/warganet | dianjurkan | ya | +| 158 | nummer | nomor | https://kbbi.web.id/nomor | umum | disarankan_cek | +| 159 | orisinil | orisinal | https://kbbi.web.id/orisinal | umum | disarankan_cek | +| 160 | original | orisinal/asli | https://kbbi.web.id/orisinal | kecuali EN | disarankan_cek | +| 161 | paskah | Paskah | https://kbbi.web.id/paskah | hari raya kapital | disarankan_cek | +| 162 | pemufakatan | pemufakatan | https://kbbi.web.id/pemufakatan | baku | disarankan_cek | +| 163 | pengelikiran | pengelicinan? | https://kbbi.web.id/pengelicinan | jangan tebak | ya | +| 164 | pensiunan | pensiunan | https://kbbi.web.id/pensiunan | baku | disarankan_cek | +| 165 | perancis | Prancis | https://kbbi.web.id/prancis | nama negara | disarankan_cek | +| 166 | Piranti | piranti | https://kbbi.web.id/piranti | kapital salah di tengah kalimat | disarankan_cek | +| 167 | plasmatik | plastik? | https://kbbi.web.id/plastik | jangan tebak | ya | +| 168 | potensil | potensial | https://kbbi.web.id/potensial | umum | disarankan_cek | +| 169 | prediksi | prediksi | https://kbbi.web.id/prediksi | baku | disarankan_cek | +| 170 | project | proyek | https://kbbi.web.id/proyek | kecuali EN | disarankan_cek | +| 171 | proyeksi | proyeksi | https://kbbi.web.id/proyeksi | baku | disarankan_cek | +| 172 | putera | putra | https://kbbi.web.id/putra | umum | disarankan_cek | +| 173 | puteri | putri | https://kbbi.web.id/putri | umum | disarankan_cek | +| 174 | qur'an | Al-Qur'an | https://kbbi.web.id/al-quran | konteks kitab | disarankan_cek | +| 175 | raport | rapor | https://kbbi.web.id/rapor | umum sekolah | disarankan_cek | +| 176 | rapot | rapor | https://kbbi.web.id/rapor | umum | disarankan_cek | +| 177 | realita | realitas | https://kbbi.web.id/realitas | umum | disarankan_cek | +| 178 | realisasi | realisasi | https://kbbi.web.id/realisasi | baku | disarankan_cek | +| 179 | rekomendasi | rekomendasi | https://kbbi.web.id/rekomendasi | baku | disarankan_cek | +| 180 | respon | respons | https://kbbi.web.id/respons | umum ilmiah | disarankan_cek | +| 181 | response | respons | https://kbbi.web.id/respons | kecuali EN | disarankan_cek | +| 182 | saos | saus | https://kbbi.web.id/saus | umum | disarankan_cek | +| 183 | sekolah dasar | sekolah dasar | https://kbbi.web.id/sekolah | baku | disarankan_cek | +| 184 | sekedar | sekadar | https://kbbi.web.id/sekadar | umum | disarankan_cek | +| 185 | silahkan | silakan | https://kbbi.web.id/silakan | sangat umum | disarankan_cek | +| 186 | sintesa | sintesis | https://kbbi.web.id/sintesis | umum ilmiah | disarankan_cek | +| 187 | sintesis | sintesis | https://kbbi.web.id/sintesis | baku | disarankan_cek | +| 188 | sopir | supir | https://kbbi.web.id/supir | cek KBBI: sopir baku? | ya | +| 189 | supir | sopir | https://kbbi.web.id/sopir | cek KBBI mana yang baku | ya | +| 190 | standardisasi | standardisasi/standarisasi | https://kbbi.web.id/standardisasi | cek KBBI | ya | +| 191 | standarisasi | standardisasi | https://kbbi.web.id/standardisasi | cek KBBI | ya | +| 192 | statment | pernyataan/statement | https://kbbi.web.id/pernyataan | typo | disarankan_cek | +| 193 | subyek penelitian | subjek penelitian | https://kbbi.web.id/subjek | frasa ilmiah | disarankan_cek | +| 194 | taqwa | takwa | https://kbbi.web.id/takwa | umum | disarankan_cek | +| 195 | tehnologi informasi | teknologi informasi | https://kbbi.web.id/teknologi | frasa | disarankan_cek | +| 196 | tempatlah | tempatlah | https://kbbi.web.id/tempatlah | partikel -lah | disarankan_cek | +| 197 | terimakasih | terima kasih | https://kbbi.web.id/terima | pisah | disarankan_cek | +| 198 | thank you | terima kasih | https://kbbi.web.id/terima | kecuali kutipan EN | disarankan_cek | +| 199 | tidak baku | tidak baku | https://kbbi.web.id/tidak | meta | disarankan_cek | +| 200 | ujian komprehensip | ujian komprehensif | https://kbbi.web.id/ujian | frasa | disarankan_cek | +| 201 | komprehensip | komprehensif | https://kbbi.web.id/komprehensif | umum | disarankan_cek | +| 202 | universitas | universitas | https://kbbi.web.id/universitas | baku | disarankan_cek | +| 203 | urutkan | urutkan | https://kbbi.web.id/urutkan | baku | disarankan_cek | +| 204 | urgent | mendesak/urgen | https://kbbi.web.id/mendesak | cek | ya | +| 205 | urgen | urgen | https://kbbi.web.id/urgen | baku? | ya | +| 206 | ustadz | ustaz | https://kbbi.web.id/ustaz | umum | disarankan_cek | +| 207 | ustad | ustaz | https://kbbi.web.id/ustaz | umum | disarankan_cek | +| 208 | walaupun | walaupun | https://kbbi.web.id/walaupun | baku serangkai | disarankan_cek | +| 209 | walau pun | walaupun | https://kbbi.web.id/walaupun | pemisahan salah | disarankan_cek | +| 210 | youtube | YouTube | https://kbbi.web.id/youtube | nama dagang | ya | +| 211 | zig-zag | zigzag | https://kbbi.web.id/zigzag | serangkai sering | ya | +| 212 | zaman | zaman | https://kbbi.web.id/zaman | baku; jaman nonbaku | disarankan_cek | +| 213 | jaman | zaman | https://kbbi.web.id/zaman | umum nonbaku | disarankan_cek | +| 214 | jin | jin | https://kbbi.web.id/jin | baku | disarankan_cek | +| 215 | jing | jin? | https://kbbi.web.id/jin | jangan tebak | ya | +| 216 | aktifitas mahasiswa | aktivitas mahasiswa | https://kbbi.web.id/aktivitas | frasa | disarankan_cek | +| 217 | ijin penelitian | izin penelitian | https://kbbi.web.id/izin | frasa | disarankan_cek | +| 218 | praktek kerja | praktik kerja | https://kbbi.web.id/praktik | frasa | disarankan_cek | +| 219 | tehnik pengumpulan data | teknik pengumpulan data | https://kbbi.web.id/teknik | frasa | disarankan_cek | +| 220 | obyek penelitian | objek penelitian | https://kbbi.web.id/objek | frasa | disarankan_cek | +| 221 | kwalitas layanan | kualitas layanan | https://kbbi.web.id/kualitas | frasa | disarankan_cek | +| 222 | resiko penelitian | risiko penelitian | https://kbbi.web.id/risiko | frasa | disarankan_cek | +| 223 | hipotesa penelitian | hipotesis penelitian | https://kbbi.web.id/hipotesis | frasa | disarankan_cek | +| 224 | metoda kualitatif | metode kualitatif | https://kbbi.web.id/metode | frasa | disarankan_cek | +| 225 | analisa kuantitatif | analisis kuantitatif | https://kbbi.web.id/analisis | frasa | disarankan_cek | +| 226 | merubah variabel | mengubah variabel | https://kbbi.web.id/mengubah | frasa | disarankan_cek | +| 227 | diijinkan dosen | diizinkan dosen | https://kbbi.web.id/diizinkan | frasa | disarankan_cek | +| 228 | sistim informasi | sistem informasi | https://kbbi.web.id/sistem | frasa | disarankan_cek | +| 229 | web site kampus | situs web kampus | https://kbbi.web.id/situs | frasa | disarankan_cek | +| 230 | download jurnal | unduh jurnal | https://kbbi.web.id/unduh | frasa | disarankan_cek | +| 231 | silahkan lihat | silakan lihat | https://kbbi.web.id/silakan | frasa | disarankan_cek | +| 232 | terimakasih atas | terima kasih atas | https://kbbi.web.id/terima | frasa | disarankan_cek | +| 233 | sekedar catatan | sekadar catatan | https://kbbi.web.id/sekadar | frasa | disarankan_cek | +| 234 | propinsi Riau | provinsi Riau | https://kbbi.web.id/provinsi | frasa | disarankan_cek | +| 235 | kongkrit misalnya | konkret misalnya | https://kbbi.web.id/konkret | frasa | disarankan_cek | +| 236 | kaedah penulisan | kaidah penulisan | https://kbbi.web.id/kaidah | frasa | disarankan_cek | +| 237 | frase kunci | frasa kunci | https://kbbi.web.id/frasa | frasa | disarankan_cek | +| 238 | respon responden | respons responden | https://kbbi.web.id/respons | frasa | disarankan_cek | +| 239 | prosentase hasil | persentase hasil | https://kbbi.web.id/persentase | frasa | disarankan_cek | +| 240 | foto copy kuesioner | fotokopi kuesioner | https://kbbi.web.id/fotokopi | frasa | disarankan_cek | +| 241 | paska pandemi | pascapandemi/pasca-pandemi | https://kbbi.web.id/pascapandemi | bentuk terikat — cek EYD | ya | + +## Cara pakai di MCP `cek_baku` + +1. Tokenisasi kata ID. +2. Cocokkan lower-case ke kolom tidak baku. +3. Jika kena, usulkan bentuk baku + URL. +4. Jika `fallback_kbbi` dan kata tidak di daftar: lookup per kata dengan rate limit; cache hasil ringkas (baku/tidak), **bukan** definisi panjang. +5. Jangan mengarang pasangan baru tanpa cek. + +Jumlah entri unik dalam file ini: **241**. diff --git a/templates/AGENTS.md b/templates/AGENTS.md index 62c9850..7556574 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -10,7 +10,7 @@ Availability is not a reason to use a tool. One primary specialist. At most one ## Closed Intent & Default Classify into exactly one intent before acting: -`repo_understand | bug | security | perf | ui_direction | ui_implement | generative_ui | motion | scroll_2d | scroll_3d | launch_film | gateway_llm | docs | ingest_md | prose | academic | longdoc_nav | web_research | browser_qa | architecture | warehouse | ops_data | video_html | demo_id | slides_pptx` +`repo_understand | bug | security | perf | ui_direction | ui_implement | generative_ui | motion | scroll_2d | scroll_3d | launch_film | gateway_llm | docs | ingest_md | prose | academic | longdoc_nav | web_research | browser_qa | architecture | warehouse | ops_data | video_html | demo_id | slides_pptx | penulis_ilmiah` 1. Repo evidence is enough → do the work. No specialist. 2. User typed a slash command → load that command's specialist. Do not substitute. @@ -43,7 +43,7 @@ Browser QA → skill `playwright-qa` (isolated verification session; builder doe Auth/secret/payment/upload/webhook/privileged/public API → `full-audit-keamanan`. Measured LCP/INP/CLS/latency/bundle → `full-performance-audit`. GitHub → `gh-axi`. Hard unknown bug → `diagnosing-bugs`. Documents (PDF/DOCX/extract/review) → `smartdoc`. Consulting PPTX / slide decks → `deck-design`. Long structured docs (tree/reasoning nav) → `pageindex`. File → Markdown ingest → `markitdown`. Reusable local knowledge → `smartbook-ingest`. -Prose AI-tells / humanize → skill `humanizer`. Slash `/unslop` is the same specialist, manual only. Technical writing structure → suggest `/technical-writing`. Academic literature / manuscript / peer-critique → skill `academic` (not `research`, not `smartdoc` unless file extract/render). Facts library/API → Context7; `research` only if repo lacking. Web/social data gathering → skill `research` (`references/web-data.md`); not `playwright-qa`. Deterministic HTML video / render HTML to MP4 → skill `hyperframes` (not `visual-studio`, not `emil-design-eng`). Editorial diagram HTML/SVG → skill `diagram-design` (not `impeccable`). Demo video aplikasi / walkthrough layar / narasi Indonesia / demo lomba → skill `id-demo-video` (bukan `hyperframes` untuk durasi panjang utuh, bukan `playwright-qa`, bukan `visual-studio`). Kartu judul HTML→MP4 tetap `hyperframes`. +Prose AI-tells / humanize → skill `humanizer`. Slash `/unslop` is the same specialist, manual only. Technical writing structure → suggest `/technical-writing`. Academic literature / manuscript / peer-critique → skill `academic` (not `research`, not `smartdoc` unless file extract/render). Facts library/API → Context7; `research` only if repo lacking. Web/social data gathering → skill `research` (`references/web-data.md`); not `playwright-qa`. Deterministic HTML video / render HTML to MP4 → skill `hyperframes` (not `visual-studio`, not `emil-design-eng`). Editorial diagram HTML/SVG → skill `diagram-design` (not `impeccable`). Demo video aplikasi / walkthrough layar / narasi Indonesia / demo lomba → skill `id-demo-video` (bukan `hyperframes` untuk durasi panjang utuh, bukan `playwright-qa`, bukan `visual-studio`). Kartu judul HTML→MP4 tetap `hyperframes`. Penulisan ilmiah ID / EYD V / kata baku KBBI / rujukan terverifikasi → skill `penulis-ilmiah` (bukan `academic`, bukan `humanizer`, bukan `smartdoc`). REST resource/status/pagination/versioning → skill `api-design`. Consumer/provider OpenAPI/AsyncAPI/Protobuf → skill `contract-first`. Live cron/CI/hook/MCP inventory keep-merge-cut → skill `automation-audit-ops`. CodeTour .tour + anchor file → skill `code-tour`. Handler vs shared-store sequential-undo → skill `click-path-audit` (not `playwright-qa`). diff --git a/tests/fixtures/penulis-ilmiah/kalimat_ejaan_baku.md b/tests/fixtures/penulis-ilmiah/kalimat_ejaan_baku.md new file mode 100644 index 0000000..6f36449 --- /dev/null +++ b/tests/fixtures/penulis-ilmiah/kalimat_ejaan_baku.md @@ -0,0 +1,40 @@ +# Fixture kalimat — ejaan & baku + +## Harus terdeteksi bermasalah + +| ID | Kalimat | Harapan | +|----|---------|---------| +| E1 | Mahasiswa belajar dikantor setiap sore. | kata depan `di` menempel → `di kantor` | +| E2 | Mereka pergi kesekolah bersama. | `ke sekolah` | +| E3 | Data diambil darirumah responden. | `dari rumah` | +| E4 | Naskah di tulis ulang oleh editor. | imbuhan salah pisah → `ditulis` | +| B1 | Aktipitas analisa perlu metoda yang tepat. | `aktivitas`, `analisis`, `metode` | +| B2 | Ijin penelitian sudah diijinkan dosen. | `izin`, `diizinkan` | +| B3 | Obyek penelitian ada di propinsi Riau. | `objek`, `provinsi` | +| B4 | Resiko kegagalan dan prosentase error dihitung. | `risiko`, `persentase` | +| B5 | Hipotesa diuji dengan tehnik statistik. | `hipotesis`, `teknik` | +| B6 | Silahkan unduh file dari web site kampus. | `silakan`, `situs web` (dan opsi `unduh`) | +| B7 | Jangan merubah variabel tanpa alasan. | `mengubah` | +| B8 | Terimakasih atas ijin dan supportnya. | `terima kasih`, `izin`; `support` → saran `dukungan`/`dukungan`) | +| S1 | Secara keseluruhan dapat disimpulkan bahwa era digital yang semakin berkembang … | frasa terlarang | + +## Harus lolos (benar) + +| ID | Kalimat | +|----|---------| +| C1 | Mahasiswa belajar di kantor setiap sore. | +| C2 | Naskah itu ditulis ulang oleh editor. | +| C3 | Aktivitas analisis memakai metode campuran. | +| C4 | Izin penelitian telah diizinkan oleh dosen pembimbing. | +| C5 | Objek penelitian berada di Provinsi Riau. | +| C6 | Silakan unduh artikel dari situs web kampus. | +| C7 | Dilarang mengubah variabel tanpa alasan. | +| C8 | Terima kasih atas izin yang diberikan. | + +## Ambigu — jangan otomatis gagal total + +| ID | Kalimat | Catatan | +|----|---------|---------| +| A1 | Dia keluar dari ruangan. | `keluar` verba, bukan `ke luar` | +| A2 | Mereka ke luar kota pada libur lebaran. | kata depan `ke` + `luar` | +| A3 | Data diolah di laboratorium. | `diolah` imbuhan; `di laboratorium` kata depan | diff --git a/tests/fixtures/penulis-ilmiah/rujukan_contoh.md b/tests/fixtures/penulis-ilmiah/rujukan_contoh.md new file mode 100644 index 0000000..9de236b --- /dev/null +++ b/tests/fixtures/penulis-ilmiah/rujukan_contoh.md @@ -0,0 +1,103 @@ +# Fixture rujukan — asli & palsu + +Status resolusi dicek lewat Crossref pada 2026-10-05 (WIB) kecuali dinyatakan lain. + +## DOI / metadata ASLI (harus → VALID jika judul selaras) + +### R1 — Deep learning + +- DOI: `10.1038/nature14539` +- Judul: Deep learning +- Penulis (family): LeCun, Bengio, Hinton +- Tahun: 2015 +- Crossref: HTTP 200 +- URL DOI: https://doi.org/10.1038/nature14539 + +### R2 — Optuna + +- DOI: `10.1145/3292500.3330701` +- Judul mengandung: Optuna +- Penulis (family): Akiba, Sano, Yanase, … +- Tahun: 2019 +- Crossref: HTTP 200 +- URL: https://doi.org/10.1145/3292500.3330701 + +### R3 — Toward unique identifiers (IEEE) + +- DOI: `10.1109/5.771073` +- Judul mengandung: Toward unique identifiers +- Penulis (family): Paskin +- Tahun: 1999 +- Crossref: HTTP 200 + +### R4 — Emotions and the big picture (contoh psikologi sosial) + +- DOI: `10.1016/j.jesp.2018.05.005` +- Judul mengandung: Emotions and the big picture +- Tahun: 2018 +- Crossref: HTTP 200 + +### R5 — Contoh Crossref “silly string” (tetap DOI nyata terdaftar) + +- DOI: `10.5555/12345678` +- Judul: Toward a Unified Theory of High-Energy Metaphysics: Silly String Theory +- Penulis: Carberry +- Catatan: DOI contoh resmi Crossref; boleh dipakai uji teknis, **jangan** dikutip sebagai literatur ilmiah serius di proposal mahasiswa. + +## Kasus TIDAK_COCOK + +### M1 — DOI benar, judul salah + +```json +{ + "doi": "10.1038/nature14539", + "judul": "A Comprehensive Survey of Blockchain Quantum Farming", + "tahun": 2015 +} +``` + +Harapan: `TIDAK_COCOK` (judul_score rendah). + +## Kasus TIDAK_DITEMUKAN / palsu + +### F1 — DOI fiktif + +```json +{ + "doi": "10.9999/fake.doi.12345.never.exists", + "judul": "Advanced Quantum Blockchain AI for Skripsi Otomatis", + "penulis": ["Fiktif, A.", "Halu, B."], + "tahun": 2024 +} +``` + +Crossref: HTTP 404 (dicek 2026-10-05). + +### F2 — Tanpa DOI, metadata halu + +```json +{ + "judul": "Superinteligensi Bahasa Indonesia untuk Segala Jurnal SINTA Level 10", + "penulis": ["Anonimus Quack"], + "tahun": 2026, + "url": "https://example.invalid/paper-halu-xyz" +} +``` + +Harapan: `TIDAK_DITEMUKAN` (dan/atau `url_hidup=false`). + +### F3 — DOI format menyerupai arXiv palsu + +```json +{ + "doi": "10.48550/arXiv.9999.99999", + "judul": "We Trained a Model on Nothing and Got 100% Accuracy", + "tahun": 2025 +} +``` + +Harapan: bukan `VALID` (404 atau tidak cocok). Jika upstream berubah, catat di laporan tes. + +## Aturan pemakaian di dokumen + +Semua item F* dan M1 **dilarang** masuk daftar pustaka akhir. Hanya R1–R4 (dan DOI VALID lain hasil `verifikasi_rujukan`) yang boleh. diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 06669ec..10b271f 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -9,6 +9,7 @@ sys.path.insert(0, str(ROOT)) import io +import json import os import shutil from contextlib import redirect_stdout @@ -18,6 +19,8 @@ from lib.install import ( # noqa: E402 cmd_crawl4ai_disable, cmd_crawl4ai_enable, + cmd_penulis_ilmiah_disable, + cmd_penulis_ilmiah_enable, cmd_scrapling_disable, cmd_scrapling_enable, cmd_install, @@ -781,6 +784,92 @@ def test_doctor_scrapling_enable_and_disable(self): self.assertEqual(rc, 0, buf.getvalue()) self.assertIn("OPTIONAL_ABSENT mcp:scrapling", buf.getvalue()) + def test_doctor_penulis_ilmiah_missing_does_not_fail(self): + self._install() + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 0, buf.getvalue()) + self.assertIn("OPTIONAL_ABSENT mcp:penulis-ilmiah", buf.getvalue()) + + def test_doctor_penulis_ilmiah_invalid_schema_fails(self): + self._install() + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + data.setdefault("mcp", {}) + data["mcp"].setdefault("servers", {}) + + # Type remote fails + data["mcp"]["servers"]["penulis-ilmiah"] = { + "type": "remote", + "url": "http://127.0.0.1:8000/mcp", + } + cfg.write_text(json.dumps(data, indent=2), encoding="utf-8") + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 1) + self.assertIn("FAIL mcp:penulis-ilmiah", buf.getvalue()) + + # Forbidden 0.0.0.0 or --http fails + data["mcp"]["servers"]["penulis-ilmiah"] = { + "type": "local", + "command": ["npx", "tsx", "mcp/penulis-ilmiah/src/index.ts", "--http", "0.0.0.0"], + } + cfg.write_text(json.dumps(data, indent=2), encoding="utf-8") + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 1) + self.assertIn("FAIL mcp:penulis-ilmiah", buf.getvalue()) + + def test_doctor_penulis_ilmiah_valid_passes(self): + self._install() + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + data.setdefault("mcp", {}) + data["mcp"].setdefault("servers", {}) + data["mcp"]["servers"]["penulis-ilmiah"] = { + "type": "local", + "command": ["npx", "tsx", "/path/to/mcp/penulis-ilmiah/src/index.ts"], + } + cfg.write_text(json.dumps(data, indent=2), encoding="utf-8") + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 0, buf.getvalue()) + self.assertIn("CONFIGURED mcp:penulis-ilmiah", buf.getvalue()) + + def test_doctor_penulis_ilmiah_enable_and_disable(self): + self._install() + rc = cmd_penulis_ilmiah_enable() + self.assertEqual(rc, 0) + cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc" + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + servers = jsonc.mcp_servers_from_config(data) + self.assertIn("penulis-ilmiah", servers) + self.assertEqual(servers["penulis-ilmiah"]["type"], "local") + self.assertTrue(any("penulis-ilmiah" in str(p) for p in servers["penulis-ilmiah"]["command"])) + self.assertNotIn("0.0.0.0", str(servers["penulis-ilmiah"])) + + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 0, buf.getvalue()) + self.assertIn("CONFIGURED mcp:penulis-ilmiah", buf.getvalue()) + + rc = cmd_penulis_ilmiah_disable() + self.assertEqual(rc, 0) + data = jsonc.loads(cfg.read_text(encoding="utf-8")) + servers = jsonc.mcp_servers_from_config(data) + self.assertNotIn("penulis-ilmiah", servers) + + buf = io.StringIO() + with redirect_stdout(buf): + rc = cmd_doctor() + self.assertEqual(rc, 0, buf.getvalue()) + self.assertIn("OPTIONAL_ABSENT mcp:penulis-ilmiah", buf.getvalue()) + def test_doctor_agent_reach_cli_findings(self): self._install() buf = io.StringIO() diff --git a/tests/test_install.py b/tests/test_install.py index 95fc8b6..1a82a4e 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -123,7 +123,7 @@ def test_fresh_install_idempotent_uninstall(self): self.assertEqual(sha256(self.sentinel), self.sentinel_hash) self.assertEqual({p.name for p in (self.tmp / ".claude").iterdir()}, {"sentinel.txt"}) skills = list((self.tmp / ".config" / "opencode" / "skills").iterdir()) - self.assertEqual(len([p for p in skills if p.is_dir()]), 50) + self.assertEqual(len([p for p in skills if p.is_dir()]), 51) cmds = list((self.tmp / ".config" / "opencode" / "commands").glob("*.md")) self.assertEqual(len(cmds), 15) self.assertEqual(cmd_skills_verify(), 0) @@ -141,7 +141,7 @@ def test_fresh_install_idempotent_uninstall(self): rc2 = cmd_install() self.assertEqual(rc2, 0) - self.assertEqual(len([p for p in (self.tmp / ".config" / "opencode" / "skills").iterdir() if p.is_dir()]), 50) + self.assertEqual(len([p for p in (self.tmp / ".config" / "opencode" / "skills").iterdir() if p.is_dir()]), 51) self.assertEqual(len(list((self.tmp / ".config" / "opencode" / "commands").glob("*.md"))), 15) bashrc = (self.tmp / ".bashrc").read_text(encoding="utf-8") self.assertEqual(bashrc.count("OPENCODEHIGHEND:BEGIN"), 1) diff --git a/tests/test_mcp.py b/tests/test_mcp.py index b39e6a7..254d0e7 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -103,6 +103,22 @@ def test_scrapling_policy_and_wanted(self): self.assertFalse(sc_wanted["wanted"]) self.assertEqual(sc_wanted["transport"], "stdio") + def test_penulis_ilmiah_policy_and_wanted(self): + policy = jsonc.loads((ROOT / "vendor" / "mcp-policy.json").read_text(encoding="utf-8")) + self.assertIn("penulis-ilmiah", policy["servers"]) + pi_policy = policy["servers"]["penulis-ilmiah"] + self.assertFalse(pi_policy["enabled"]) + self.assertEqual(pi_policy["transport"], "stdio") + self.assertEqual(pi_policy["command"], "npx") + self.assertEqual(pi_policy["args"], ["tsx", "mcp/penulis-ilmiah/src/index.ts"]) + + wanted = jsonc.loads((ROOT / "vendor" / "mcp-wanted.json").read_text(encoding="utf-8")) + self.assertIn("penulis-ilmiah", wanted["servers"]) + pi_wanted = wanted["servers"]["penulis-ilmiah"] + self.assertEqual(pi_wanted["status"], "FOREIGN_ON_DEMAND") + self.assertFalse(pi_wanted["wanted"]) + self.assertEqual(pi_wanted["transport"], "stdio") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_migration.py b/tests/test_migration.py index ab3201d..b7e598b 100644 --- a/tests/test_migration.py +++ b/tests/test_migration.py @@ -106,7 +106,7 @@ def test_migrate_claudebestfriend_1_4_2_to_ocbf(self): (self.tmp / ".local" / "share" / "opencode-highend" / "components" / "installer").exists() ) skills = list((self.tmp / ".config" / "opencode" / "skills").iterdir()) - self.assertEqual(len([p for p in skills if p.is_dir()]), 50) + self.assertEqual(len([p for p in skills if p.is_dir()]), 51) with redirect_stdout(io.StringIO()): self.assertEqual(cmd_verify(), 0) self.assertEqual(cmd_doctor(), 0) diff --git a/tests/test_penulis_ilmiah.py b/tests/test_penulis_ilmiah.py new file mode 100644 index 0000000..79c144e --- /dev/null +++ b/tests/test_penulis_ilmiah.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import re +import shutil +import subprocess +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +import sys + +sys.path.insert(0, str(ROOT)) +from lib import jsonc # noqa: E402 +from lib.common import load_policy, repo_root # noqa: E402 + + +class PenulisIlmiahTests(unittest.TestCase): + def test_skill_structure_and_frontmatter(self): + skill_dir = ROOT / "skills" / "penulis-ilmiah" + skill_md = skill_dir / "SKILL.md" + self.assertTrue(skill_md.is_file()) + + text = skill_md.read_text(encoding="utf-8") + self.assertTrue(text.startswith("---")) + parts = text.split("---", 2) + self.assertGreaterEqual(len(parts), 3) + + fm_text = parts[1] + self.assertIn("name: penulis-ilmiah", fm_text) + self.assertIn("compatibility: opencode", fm_text) + self.assertIn("license: MIT", fm_text) + self.assertIn("lang: id", fm_text) + self.assertIn("mcp: penulis-ilmiah", fm_text) + + # Boundaries & rules + self.assertIn("academic", text) + self.assertIn("humanizer", text) + self.assertIn("smartdoc", text) + self.assertIn("research", text) + self.assertIn("fail-closed", text.lower()) + + def test_references_and_kata_baku_catalog(self): + ref_dir = ROOT / "skills" / "penulis-ilmiah" / "references" + self.assertTrue((ref_dir / "README.md").is_file()) + self.assertTrue((ref_dir / "kata_baku.md").is_file()) + self.assertTrue((ref_dir / "gaya_tulisan.md").is_file()) + self.assertTrue((ref_dir / "gaya_sitasi.md").is_file()) + self.assertTrue((ref_dir / "frasa_terlarang.md").is_file()) + self.assertTrue((ref_dir / "eyd" / "README.md").is_file()) + self.assertTrue((ref_dir / "eyd" / "04_kata_depan.md").is_file()) + self.assertTrue((ref_dir / "eyd" / "05_imbuhan_kata_turunan.md").is_file()) + self.assertTrue((ref_dir / "eyd" / "10_tanda_baca.md").is_file()) + + # Validate kata_baku.md has >= 100 entries + kb_text = (ref_dir / "kata_baku.md").read_text(encoding="utf-8") + entries = 0 + for line in kb_text.splitlines(): + line = line.strip() + if line.startswith("|") and not line.startswith("| #") and not line.startswith("|---"): + cols = [c.strip() for c in line.split("|")] + if len(cols) >= 6 and cols[2] and cols[3]: + entries += 1 + self.assertGreaterEqual(entries, 100, f"Expected >= 100 kata baku entries, found {entries}") + + def test_mcp_package_files(self): + mcp_dir = ROOT / "mcp" / "penulis-ilmiah" + self.assertTrue((mcp_dir / "package.json").is_file()) + self.assertTrue((mcp_dir / "tsconfig.json").is_file()) + self.assertTrue((mcp_dir / "README.md").is_file()) + self.assertTrue((mcp_dir / ".env.example").is_file()) + self.assertTrue((mcp_dir / "src" / "index.ts").is_file()) + self.assertTrue((mcp_dir / "src" / "tools" / "cek_ejaan.ts").is_file()) + self.assertTrue((mcp_dir / "src" / "tools" / "cek_baku.ts").is_file()) + self.assertTrue((mcp_dir / "src" / "tools" / "cari_rujukan.ts").is_file()) + self.assertTrue((mcp_dir / "src" / "tools" / "verifikasi_rujukan.ts").is_file()) + self.assertTrue((mcp_dir / "src" / "tools" / "format_sitasi.ts").is_file()) + + pkg = json.loads((mcp_dir / "package.json").read_text(encoding="utf-8")) + self.assertEqual(pkg["name"], "penulis-ilmiah-mcp") + self.assertIn("@modelcontextprotocol/sdk", pkg["dependencies"]) + self.assertIn("citation-js", pkg["dependencies"]) + self.assertIn("zod", pkg["dependencies"]) + + def test_mcp_tools_execution(self): + mcp_dir = ROOT / "mcp" / "penulis-ilmiah" + dist_entry = mcp_dir / "dist" / "tools" / "cek_ejaan.js" + + node_bin = shutil.which("node") + if not node_bin: + self.skipTest("node binary not found on PATH") + + if not dist_entry.is_file(): + npm_bin = shutil.which("npm") + if not npm_bin: + self.skipTest("npm binary not found on PATH to build mcp/penulis-ilmiah") + if not (mcp_dir / "node_modules").is_dir(): + subprocess.run([npm_bin, "ci"], cwd=mcp_dir, check=True, timeout=180, capture_output=True) + subprocess.run([npm_bin, "run", "build"], cwd=mcp_dir, check=True, timeout=120, capture_output=True) + + self.assertTrue(dist_entry.is_file()) + + # Script running inside node to test tools unit behavior + script = """ +import { cekEjaan } from "./dist/tools/cek_ejaan.js"; +import { cekBaku } from "./dist/tools/cek_baku.js"; +import { verifikasiRujukan } from "./dist/tools/verifikasi_rujukan.js"; +import { formatSitasi } from "./dist/tools/format_sitasi.js"; + +async function testAll() { + const e1 = await cekEjaan({ teks: "Mahasiswa belajar dikantor setiap sore.", pakai_hunspell: false, bahasa: "id" }); + if (e1.ok || e1.temuan.length === 0 || e1.temuan[0].saran[0] !== "di kantor") { + throw new Error("E1 failed: " + JSON.stringify(e1)); + } + + const e4 = await cekEjaan({ teks: "Naskah di tulis ulang oleh editor.", pakai_hunspell: false, bahasa: "id" }); + if (e4.ok || e4.temuan.length === 0 || e4.temuan[0].saran[0] !== "ditulis") { + throw new Error("E4 failed: " + JSON.stringify(e4)); + } + + const b1 = await cekBaku({ teks: "Aktipitas analisa perlu metoda yang tepat.", fallback_kbbi: false, max_lookup: 20 }); + const b1Words = b1.temuan.map(t => t.kata.toLowerCase()); + if (!b1Words.includes("aktipitas") || !b1Words.includes("analisa") || !b1Words.includes("metoda")) { + throw new Error("B1 failed: " + JSON.stringify(b1)); + } + + const vFake = await verifikasiRujukan({ doi: "10.9999/fake.doi.12345.never.exists", ambang_judul: 0.85 }); + if (vFake.status !== "TIDAK_DITEMUKAN") { + throw new Error("vFake status should be TIDAK_DITEMUKAN, got " + vFake.status); + } + + const fFail = await formatSitasi({ + gaya: "apa7", + item: { doi: "10.9999/fake.doi.12345.never.exists", judul: "Nonexistent Paper" }, + wajib_terverifikasi: true + }); + if (!("error" in fFail) || fFail.error.code !== "REF_NOT_VERIFIED") { + throw new Error("fFail should reject non-verified reference with REF_NOT_VERIFIED"); + } + + console.log("NODE_TESTS_PASS"); +} + +testAll().catch(err => { + console.error(err); + process.exit(1); +}); +""" + proc = subprocess.run( + ["node", "--input-type=module", "-e", script], + cwd=mcp_dir, + capture_output=True, + text=True, + timeout=30 + ) + self.assertEqual(proc.returncode, 0, f"STDOUT: {proc.stdout}\nSTDERR: {proc.stderr}") + self.assertIn("NODE_TESTS_PASS", proc.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_routing.py b/tests/test_routing.py index f2372b5..277d888 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -282,15 +282,16 @@ def test_web_research_routing_boundary(self): docs = (ROOT / "docs" / "routing.md").read_text(encoding="utf-8") self.assertIn("`web_research` | `research`", docs) - # 25 closed intents in templates/AGENTS.md, rules/00-routing.md, docs/routing.md + # 26 closed intents in templates/AGENTS.md, rules/00-routing.md, docs/routing.md intents_agents = [ x.strip("` ") for x in self.agents.split("Classify into exactly one intent before acting:\n`")[1] .split("`\n")[0] .split("|") ] - self.assertEqual(len(intents_agents), 25) + self.assertEqual(len(intents_agents), 26) self.assertIn("web_research", intents_agents) + self.assertIn("penulis_ilmiah", intents_agents) # Boundaries & references self.assertIn("references/web-data.md", self.agents) @@ -314,6 +315,22 @@ def test_web_research_routing_boundary(self): ba_skill = (ROOT / "skills" / "browser-act" / "SKILL.md").read_text(encoding="utf-8") self.assertIn("web-data.md", ba_skill) + def test_penulis_ilmiah_routing_boundary(self): + self.assertIn("penulis_ilmiah", self.agents) + self.assertIn("`penulis_ilmiah` | `penulis-ilmiah`", self.routing) + docs = (ROOT / "docs" / "routing.md").read_text(encoding="utf-8") + self.assertIn("`penulis_ilmiah` | `penulis-ilmiah`", docs) + self.assertIn("skill `penulis-ilmiah`", self.agents) + self.assertIn("EYD V", self.agents) + self.assertIn("kata baku KBBI", self.agents) + self.assertIn("bukan `academic`", self.agents) + + skill = (ROOT / "skills" / "penulis-ilmiah" / "SKILL.md").read_text(encoding="utf-8") + self.assertIn("academic", skill) + self.assertIn("humanizer", skill) + self.assertIn("smartdoc", skill) + self.assertIn("fail-closed", skill.lower()) + def test_no_context_guard_rule(self): self.assertFalse((ROOT / "rules" / "04-context-guard.md").exists()) diff --git a/tests/test_skills.py b/tests/test_skills.py index 2baf0b6..7cb21b6 100644 --- a/tests/test_skills.py +++ b/tests/test_skills.py @@ -56,11 +56,11 @@ def jaccard(left: set[str], right: set[str]) -> float: class SkillPolicyTests(unittest.TestCase): def test_counts(self): allow, skills, model, manual = load_policy(ROOT) - self.assertEqual(len(allow), 65) - self.assertEqual(len(model), 50) + self.assertEqual(len(allow), 66) + self.assertEqual(len(model), 51) self.assertEqual(len(manual), 15) self.assertEqual(set(allow), set(skills)) - for name in ("supabase-ops", "mongodb-ops", "vercel-ops", "business-motion-film", "ninerouter", "markitdown", "id-demo-video", "json-render", "deck-design", "pageindex"): + for name in ("supabase-ops", "mongodb-ops", "vercel-ops", "business-motion-film", "ninerouter", "markitdown", "id-demo-video", "json-render", "deck-design", "pageindex", "penulis-ilmiah"): self.assertIn(name, allow) self.assertIn(name, model) self.assertIn("demo-video", manual) diff --git a/vendor/license-audit.json b/vendor/license-audit.json index c5a3f27..53f4437 100644 --- a/vendor/license-audit.json +++ b/vendor/license-audit.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "product": "OpenCodeHighEnd", - "productVersion": "0.1.17", + "productVersion": "0.1.18", "note": "Evidence-based. A missing frontmatter license is not a grant. Adapted \u2260 first-party.", "skills": { "demo-video": { @@ -328,6 +328,11 @@ "license": "MIT", "evidence": "VectifyAI/PageIndex 6d23caf4 + SKILL.md frontmatter + skills/pageindex/NOTICE.md", "redistribution": "mit" + }, + "penulis-ilmiah": { + "license": "MIT", + "evidence": "first-party penulis-ilmiah export + SKILL.md frontmatter", + "redistribution": "mit" } } } diff --git a/vendor/mcp-policy.json b/vendor/mcp-policy.json index 5407640..e764bf1 100644 --- a/vendor/mcp-policy.json +++ b/vendor/mcp-policy.json @@ -72,6 +72,12 @@ "transport": "stdio", "command": "uvx", "args": ["--from", "scrapling[ai]==0.4.15", "scrapling", "mcp"] + }, + "penulis-ilmiah": { + "enabled": false, + "transport": "stdio", + "command": "npx", + "args": ["tsx", "mcp/penulis-ilmiah/src/index.ts"] } } } diff --git a/vendor/mcp-wanted.json b/vendor/mcp-wanted.json index f3c5cd7..50b73c0 100644 --- a/vendor/mcp-wanted.json +++ b/vendor/mcp-wanted.json @@ -95,6 +95,15 @@ "command": "uvx", "args": ["--from", "scrapling[ai]==0.4.15", "scrapling", "mcp"], "status": "FOREIGN_ON_DEMAND" + }, + "penulis-ilmiah": { + "wanted": false, + "ownedIfAdded": false, + "scope": "user", + "transport": "stdio", + "command": "npx", + "args": ["tsx", "mcp/penulis-ilmiah/src/index.ts"], + "status": "FOREIGN_ON_DEMAND" } } } diff --git a/vendor/provenance.json b/vendor/provenance.json index ce4ca84..d08c1bc 100644 --- a/vendor/provenance.json +++ b/vendor/provenance.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "product": "OpenCodeHighEnd", - "productVersion": "0.1.17", + "productVersion": "0.1.18", "firstPartyLicense": "MIT", "components": [ { @@ -754,6 +754,15 @@ "modified": false, "redistribution": "mit", "notes": "First-party operational specialist for Vercel deployment, Next.js hosting, and vercel.json configurations." + }, + { + "component": "penulis-ilmiah", + "path": "skills/penulis-ilmiah", + "upstream": "first-party", + "license": "MIT", + "modified": false, + "redistribution": "mit", + "notes": "First-party specialist for Bahasa Indonesia academic drafting, EYD V spelling, KBBI standard words, and fail-closed citation verification." } ] } diff --git a/vendor/skill-allowlist.txt b/vendor/skill-allowlist.txt index 8c012b2..803aa08 100644 --- a/vendor/skill-allowlist.txt +++ b/vendor/skill-allowlist.txt @@ -43,6 +43,7 @@ matt-code-review mongodb-ops ninerouter pageindex +penulis-ilmiah playwright-qa prototype reflect diff --git a/vendor/skill-policy.json b/vendor/skill-policy.json index b8a96ec..9a07c10 100644 --- a/vendor/skill-policy.json +++ b/vendor/skill-policy.json @@ -137,6 +137,9 @@ "pageindex": { "invocation": "model" }, + "penulis-ilmiah": { + "invocation": "model" + }, "playwright-qa": { "invocation": "model" }, diff --git a/vendor/sources.json b/vendor/sources.json index f1dbb13..ed818c8 100644 --- a/vendor/sources.json +++ b/vendor/sources.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "product": "OpenCodeHighEnd", - "productVersion": "0.1.17", + "productVersion": "0.1.18", "sources": { "codebase-memory": { "repository": "https://github.com/DeusData/codebase-memory-mcp", @@ -77,6 +77,14 @@ "sdistSha256": "72406900f437316209dd05d9853dae04420f1cab0e11071604bb4588b6e2f713", "note": "Optional MCP via uvx --from scrapling[ai]==0.4.15 scrapling mcp. Not vendored. Enabled via opencode-he scrapling enable. Do not run scrapling install (requires sudo / playwright install-deps)." }, + "penulis-ilmiah": { + "package": "penulis-ilmiah-mcp", + "version": "0.1.0", + "transport": "stdio", + "via": "npx", + "status": "foreign-on-demand", + "note": "Optional first-party MCP server for Bahasa Indonesia academic writing, EYD V, KBBI standard words, and verified citations. Enabled via opencode-he penulis-ilmiah enable." + }, "crawl4ai": { "repository": "https://github.com/unclecode/crawl4ai", "version": "0.9.4",