VisualDoc
by Alvnvnc
README.md
# VisualDoc
> Visual-layout QA untuk dokumen — *"Playwright, tapi untuk DOCX / Google Docs / Word."*
> Satu MCP server yang dipakai identik oleh **Claude Code** dan **OpenCode**:
> **render → ukur → cek → patch → diff → ulangi.**
**Status: Fase 0 + Fase 1 (+ penguatan) ✅ terverifikasi end-to-end** lewat klien MCP
stdio sungguhan, **47 pytest hijau**. Loop tertutup: `doc_autofix` membawa dokumen contoh
dari `{critical:1}` → `{critical:0}` dalam 1 iterasi. Penguatan: **anchor stabil
`w14:paraId`**, **mode `review`/`safe`/`full`**, **regression guard** (revisi yang
memunculkan isu baru → rollback), dan **guardrails input**: validasi DOCX, deteksi halaman
di luar rentang, serta **error terstruktur `{ok:false, error{code}}`** — menutup celah
"laporan hijau padahal salah".
```
doc_render_report → 1 critical (table_overflow) + 1 warning (image_low_dpi)
doc_autofix → baseline{critical:1} → final{critical:0} accepted=True
iter1: set_table_width(el-4) → resolved [p1:table_overflow]
unresolved: [p1:image_low_dpi] (butuh aset resolusi lebih tinggi)
```
## Loop (Fase 1)
```
doc_get_structure ──► el-N (posisional) + anchor (w14:paraId, stabil) + version_hash
│
doc_apply_patch(ops, expected_version_hash) ──► revisi + validasi round-trip
│ target = el-N | anchor (file asli tak disentuh)
doc_render_report(revisi) ──► ukur ulang
│
doc_diff_versions ──► resolved / new(regresi) / delta
↺ (dibungkus jadi satu tool: doc_autofix — mode review|safe|full)
berhenti saat tak ada yang bisa diperbaiki / max_iter / regression guard
(revisi yang memunculkan warning baru → dibuang, versi sebelumnya dipertahankan)
```
Loop hidup sebagai **satu tool** (`doc_autofix`), bukan Claude Code hook — supaya
portabel ke OpenCode ([docs/01-compatibility.md](docs/01-compatibility.md)). Agen juga
bisa menyetir loop lebih halus sendiri via `doc_get_structure` + `doc_apply_patch`.
## Struktur
```
namanya-nanti/
├── .mcp.json · pyproject.toml · README.md
├── docs/ 00-concept · 01-compatibility · 02-preview-model (DOCX vs LaTeX)
├── src/visualdoc/
│ ├── server.py # MCP server (FastMCP, stdio) — 7 tool
│ ├── core/ # FORMAT-AGNOSTIC (bekerja atas PDF apa pun)
│ │ ├── geometry.py # PDF -> bbox/font/DPI/tabel
│ │ ├── rules.py # rule engine (+ engine_sensitivity)
│ │ ├── report.py # analyze_pdf()
│ │ ├── diff.py # diff_reports() before/after
│ │ └── raster.py # PDF page -> PNG (kanal visual opsional)
│ └── adapters/docx/ # FORMAT-SPECIFIC
│ ├── render.py # DOCX -> PDF (LibreOffice)
│ ├── meta.py # margin/style OOXML
│ ├── patch.py # get_structure + ops aman + validasi round-trip
│ ├── autofix.py # loop deterministik (structure-driven, render-verified)
│ └── pipeline.py # build_report()
├── rules/general.yaml · configs/ · scripts/
└── workspace/ original/ revisions/ renders/ reports/
```
## Tool (7)
| Tool | Fungsi |
|------|--------|
| `doc_selftest()` | Cek kesehatan (soffice/PyMuPDF/python-docx) + daftar `patch_ops`. |
| `doc_render_report(document, pages?, rules?)` | Render + ukur + cek → JSON (`summary.headline`, `pages[]`). **Tanpa gambar.** |
| `doc_get_structure(document, assign_anchors?, output?)` | Elemen ter-alamat: `el-N` + `anchor` (`w14:paraId`, stabil) + `version_hash` + `text_column_mm`. `assign_anchors` → `normalized_output`. |
| `doc_apply_patch(document, ops, expected_version_hash?, output?)` | Op aman ke **salinan** (target `el-N` \| `anchor`) + validasi round-trip. |
| `doc_autofix(document, rules?, max_iter?, mode?)` | Loop tertutup + **regression guard** → revisi final + `history`/diff. `mode`: `review`\|`safe`\|`full`. |
| `doc_diff_versions(before, after, rules?)` | Diff dua versi: `resolved` / `new` / delta. |
| `doc_get_page_image(document, page?, dpi?)` | **Opsional**: PNG satu halaman (mahal token; pakai hemat). |
**Op aman** (`doc_apply_patch`): `set_spacing`, `set_keep_with_next`,
`set_page_break_before`, `set_paragraph_style`, `set_font`, `set_table_width`.
Tiap op melapor `applied`/`error` independen; op salah tak menggagalkan yang benar.
`target` bisa `el-N` (posisional) **atau** `anchor` (`w14:paraId` — bertahan lintas-edit;
`doc_get_structure(assign_anchors=true)` menuliskannya ke `normalized_output`).
**Mode `doc_autofix`**: `review` (rencana saja) · `safe` (invariant: fit tabel) ·
`full` (safe + heading `keep_with_next` + clamp spacing). Warning membawa
`engine_sensitivity` (`invariant` benar di renderer manapun; `dependent` hanya valid untuk
layout LibreOffice ini) — lihat [docs/02-preview-model.md](docs/02-preview-model.md).
**Kontrak error**: tiap tool mengembalikan `ok: true` saat sukses, atau
`{ok: false, error: {code, message, detail?}}` saat gagal — `code` ∈ {`not_found`,
`invalid_docx`, `invalid_rules`, `pages_out_of_range`, `stale_version`, `bad_argument`,
`no_ops`, `no_ops_applied`, `render_unavailable`, `render_failed`, `internal`}. Input DOCX &
rule-pack divalidasi (bukan-DOCX / `.doc` lama / korup / YAML rusak ditolak), render yang
gagal/timeout terbedakan dari input salah, dan halaman di luar rentang tak pernah menghasilkan
laporan "hijau" palsu (semua di luar rentang → error; sebagian → di-flag). `apply_patch` yang
0-perubahan tidak menulis file (`no_ops`/`no_ops_applied`).
**Keandalan tabel (#4)**: `table_overflow` dikoroborasi dengan **lebar OOXML** (`patch._table_width_mm`)
— jumlah kolom di pesan direkonsiliasi ke angka OOXML yang otoritatif (mis. render 7 → OOXML 8),
tiap tabel render dijembatani ke `element_id`, dan overflow yang tak terdeteksi renderer (mis.
tabel tanpa border) tetap muncul sebagai isu struktural, bukan hilang diam-diam.
## Prinsip desain
**Semua analisis di server, dikembalikan sebagai teks/JSON — bukan gambar ke model.**
Rule engine jalan murni atas *angka* geometri (PyMuPDF), tanpa vision → murah token,
deterministik, jalan di kedua klien bahkan dengan model text-only.
Alasan: [docs/01-compatibility.md](docs/01-compatibility.md).
## Quickstart
```bash
# Prasyarat sistem (sekali):
sudo apt-get install -y --no-install-recommends libreoffice-writer python3-venv fonts-liberation
python3 -m venv .venv && .venv/bin/pip install -e .
# Coba:
.venv/bin/python scripts/make_sample.py # -> workspace/original/sample.docx
.venv/bin/python scripts/mcp_smoke.py # uji 7 tool end-to-end lewat protokol MCP
.venv/bin/mcp dev src/visualdoc/server.py # (opsional) MCP Inspector interaktif
# Test (mengunci semua perilaku agar tak regres):
.venv/bin/pip install -e ".[test]" && .venv/bin/python -m pytest
```
## Registrasi di klien
- **Claude Code** — `.mcp.json` sudah ada di root: `/mcp` → approve `visualdoc`
(perlu restart sesi; server MCP tak hot-load).
- **OpenCode** — salin `configs/opencode.json` ke `opencode.json` project Anda.
Contoh perintah ke agen: *"jalankan doc_autofix pada workspace/original/sample.docx,
ringkas apa yang diperbaiki dan apa yang tersisa."*
## Batasan (jujur, by design)
- **Anchor elemen stabil** (`w14:paraId`) sudah ada; pemetaan elemen↔**halaman** presisi
(korelasi render↔OOXML, mis. inject-marker) masih penyempurnaan lanjut.
- **Auto-fix**: table overflow (`safe`) + heading `keep_with_next` & clamp spacing (`full`).
`image_low_dpi` & `orphan_heading` dilaporkan, belum di-auto-fix (butuh aset/keputusan).
- **Satu engine (LibreOffice)** — temuan `dependent` (page-break/orphan) perlu engine
target (Word) untuk kepercayaan penuh. Multi-engine = Fase 4.
- **Belum ada `preview-ui/`** (viewer manusia) dan adapter non-DOCX. Roadmap →
[docs/00-concept.md](docs/00-concept.md).
TDQS
B3.4/5.0
Scored across 2 tools
Disambiguation5/5
The two tools have clearly distinct purposes: one for comparing versions via diff, the other for extracting a single page image. There is no overlap.
Naming Consistency5/5
Both tools follow a consistent verb_noun pattern with the 'doc_' prefix: doc_diff_versions and doc_get_page_image.
Tool Count2/5
Only two tools are provided for a documentation visualization server. The description references a primary tool (doc_render_report) that is not included, making the surface too thin for its stated purpose.
Completeness1/5
The primary signal for the server (the deterministic report without images) is missing as a tool. There is no way to get the main report, only a diff and a page image, leaving a severe gap in coverage.
Maintenance
ActivityStale
ResponsivenessNo issues