Skip to main content
Glama
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