Normalizuje wyjscie darmowego RODO-safe stacku OCR/PDF (opendataloader-pdf, pdftotext, Chandra OCR, OCR PATRONa/Gaius-Lex) do jednego audytowalnego kontraktu {block_type, bbox, text, confidence}...
Kazdy szczebel naszej drabinki PDF (pdftotext -> anydoc -> markitdown -> opendataloader -> liteparse -> Chandra -> vision) zwraca wynik w innym formacie. Ten skill sprowadza te formaty do JEDNEGO kontraktu, ktory od razu odpowiada na trzy pytania:
citation-grounding-pl.Zamkniety jest MODEL Mistral OCR 4, nie idea. Odtwarzamy kontrakt na wlasnym, lokalnym, RODO-safe stacku - skladamy puzzle, nie wynajdujemy kola.
Wersja zyje w stalej contract.CONTRACT_VERSION; ten naglowek i przyklad ponizej
sa zakotwiczone testem tests/test_dokumentacja_zakotwiczona.py (D1).
{
"doc_id": "<sha256 wejscia>",
"contract_version": "1.2.0",
"source": {"path": "...", "engine": "opendataloader|pdftotext|chandra|gaius|vlm-html|pdf-inspector|liteparse", "engine_variant": "default|google_doc_ai|null", "pages": N},
"blocks": [
{"id": "b0001", "page": 1, "bbox": [x0,y0,x1,y1]|null,
"block_type": "title|paragraph|table|list|equation|signature|stamp|figure|header|footer|unknown",
"text": "...", "confidence": 0.0-1.0|null, "flags": ["partial","pii_suspected","pii:pesel","sensitive_block","signature_suspected",...]}
],
"gating": {
"threshold": 0.85,
"review_required": ["b0003"], "auto_approved": ["b0001"],
"ungroundable": ["b0007"],
"verdict": "ok|degraded|failed",
"note": "...",
"counts": {"total": 11, "review_required": 1, "auto_approved": 10, "ungroundable": 0}
},
"redaction_candidates": ["b0004"],
"meta": {"created_at": "ISO-8601"}
}
bbox znormalizowany 0-1 (przenosny miedzy DPI). Silnik bez bbox -> null + flaga partial.confidence == null (partial) -> zawsze do review_required (konserwatywnie; nie wiemy = czlowiek patrzy).Do 1.1.0 istniala jedna os - confidence silnika. To mieszalo dwa twierdzenia,
ktore moga sie rozjechac:
| os | pyta o | pole |
|---|---|---|
| PEWNOSC | czy silnik jest pewny odczytanych znakow | review_required / auto_approved |
| UGRUNTOWANIE | czy blok da sie WSKAZAC w dokumencie (ma bbox) | ungroundable |
Blok z confidence: 0.99 i bbox: null jest pewny i jednoczesnie niecytowalny:
nie da sie go podswietlic ani przypiac do strony. Wersja 1.1.0 wpisywala go do
auto_approved i nikt sie nie dowiadywal.
verdict jest trojstanowy, slownictwem i kodami wyjscia zgodny z routing_gate.py
(ok/degraded/failed = 0/10/20; stala contract.GATING_EXIT).
Zero blokow = failed, nigdy ok - bramka, ktora przy pustym wejsciu mowi
"nic do przegladu", przepuszcza dokument, ktorego nikt nie przeczytal.
scripts/evidence.py, od 2026-08-30)Most do groundingu nie mowi juz "znalazlem / nie znalazlem". Kazdy cytat dostaje dowod o dwoch ortogonalnych osiach plus pelny mianownik trafien.
kind (rodzaj dowodu) |
sila | znaczenie |
|---|---|---|
verbatim |
4 | wystepuje w bloku bajt w bajt |
normalized |
3 | wystepuje po normalizacji typograficznej; lokalizacja doslowna, ale glify zrodla roznia sie od cytatu |
observed |
2 | jest w dokumencie, lecz nie w jednym bloku (granica blokow albo zbyt wiele trafien) |
derived |
0 | brak lokalizacji doslownej; przypisanie do calosci |
resolution (rozdzielczosc) |
sila |
|---|---|
span (zakres znakowy w bloku) |
4 |
block |
3 |
page |
2 |
document |
1 |
none |
0 |
Bramka grounding_bridge.gate() ma trzy warunki naraz: rodzaj, rozdzielczosc
oraz jednoznacznosc. Mocny dowod na piec roznych miejsc nie jest mocnym
dowodem na jedno - dlatego przy wiecej niz jednym trafieniu anchor_resolved
jest null, a wszystkie kandydatury leza w evidence.anchors. Wieloznacznosc
idzie do raportu, nigdy do auto-wyboru.
python scripts/grounding_bridge.py kontrakt.json --quotes cytaty.txt --pretty
python scripts/grounding_bridge.py kontrakt.json --quotes cytaty.txt --min-kind verbatim --min-resolution span
Exit 10 gdy cokolwiek nie przeszlo bramki (trojstan, nie cisza).
Wersja 1.x mostu gubila cytaty po cichu, bo jej normalizacja nie znala znakow, ktore polski PDF wstawia naprawde:
| znak | gdzie wystepuje | skutek w 1.x |
|---|---|---|
U+2011 dywiz nielamliwy |
sygnatury akt (II‑CSK 118/24) |
cytat NIE znaleziony |
U+00AD miekki dywiz |
wnetrze slow w justowanym akapicie | cytat NIE znaleziony |
U+2212 minus, U+200B zerowa szerokosc |
tabele, wklejki | cytat NIE znaleziony |
| przeniesienie wyrazu (`apela- | ||
| cje`) | lamanie linii | znaleziony, ale bez zakresu |
Do tego ł/Ł nie maja dekompozycji NFD, a reszta polskich liter ma - klasyczne
"dziala dla osmiu z dziewieciu".
evidence.normalize_with_map() sklada NFC grupami (znak bazowy + laczace) i
zwraca mape offsetow, dzieki czemu zakres wraca na ORYGINALNE glify zrodla.
Fixture wierny zjawisku: tests/fixtures/pismo_pl_typografia.sample.json.
cd doc-intel-contract-pl # katalog tego skilla
python scripts/normalize.py --engine opendataloader wyjscie.json --pretty
python scripts/normalize.py --engine pdftotext dokument.txt --threshold 0.9
cat wyjscie.json | python scripts/normalize.py --engine opendataloader -
Exit: 0 = kontrakt schema-valid, 2 = blad wejscia / kontrakt niepoprawny (pasuje pod CI / pre-commit).
routing_gate.pyOdpowiada na pytanie, ktore dotad rozstrzygalo oko: czy ten dokument w ogole da
sie przeczytac tekstowo i ktorym szczeblem. Werdykt trojstanowy z pelnym
mianownikiem, kod wyjscia 0/10/20 (ok/degraded/failed).
python scripts/routing_gate.py AKTA.pdf --pretty
python scripts/routing_gate.py *.pdf *.docx --quiet # tylko to, co nie jest ok
Lapie trzy rzeczy, ktorych zaden konwerter nie zglasza:
degraded + numery stron do OCR.liteparse: degraded + szczebel
4/liteparse-ocr-cpu (tekst z OCR czytac tak, cytowac po weryfikacji). Bez niego:
failed, eskalacja (Chandra wymaga GPU). DOC_INTEL_NO_CPU_OCR=1 wylacza szczebel.chart1.xml = exit 0, stderr pusty, znika cala tabela). Bramka nazywa
czesc PRZED konwersja.PDF wymaga pip install pdf-inspector (MIT). Jego brak = failed, nigdy ciche ok.
liteparse_extract.py + --engine liteparsepython scripts/liteparse_extract.py SKAN.pdf --tessdata KATALOG_TESSDATA > skan.json # exit 20 = niekompletny
python scripts/normalize.py --engine liteparse skan.json --pretty
Ekstraktor porownuje total_pages z liczba zwroconych stron (biblioteka domyslnie
obcina do 1000 stron bez bledu - na dluzszym PDF jedynym sladem jest total_pages). Adapter:
brakujaca strona = blok missing_page, raster bez tekstu = unreadable, $ przed
liczba w OCR = § z flaga repaired_paragraf, pewnosc bloku = 10. percentyl
pewnosci slow (+ weak_word), tekst natywny = pewnosc null + native_text.
Bloki ukladu biblioteki uzywane tylko wtedy, gdy nie gubia zadnej litery ani cyfry
strony (residual_alnum); inaczej wlasne grupowanie + layout_blocks_lossy.
split_packet.pypython scripts/split_packet.py TECZKA.pdf --pretty # exit 0/10/20
python scripts/split_packet.py strony.json --reguly contract/kategorie_pism.json
python scripts/split_packet.py TECZKA.pdf --eksport wynik/ # PDF per pismo, hash zrodla sprawdzany
Lokalnie, bez LLM. Decyzja per strona z jawnymi sygnalami (naglowek typu pisma,
formula "W imieniu RP", sygnatura w pierwszej linii, zmiana sygnatury, numer
strony, kontynuacja zdania, blok podpisow) - dowod w page_decisions jest
prawdziwy, bo silnik jest deterministyczny. Niezmienniki: kazda strona dokladnie
raz, pusta strona nie otwiera pisma, strona nieczytelna = failed, dwa sasiednie
pisma tej samej kategorii = dwa segmenty. Decyzje blisko progu i naglowki bez
sygnatury ida do przegladu (degraded).
Pomiar 2026-09-21 na teczkach zlozonych z publicznych orzeczen (granice znane z konstrukcji; strojenie na dev, raport na odlozonym test): patrz "Status / roadmap". Granica dowodu: teczki sa syntetyczne (tekst orzeczen dzielony na strony), bez szumu OCR i bez pism stron (pozwy, pelnomocnictwa) - to nie jest pomiar na prawdziwej teczce. Na zeskanowanych aktach silnik mocno zaniza liczbe pism (przebieg 2026-09-21): protokoly, notatki i pisma organow nie maja struktury orzeczenia, wiec sygnaly z tej listy na nich nie odpalaja. Wynik na aktach to szkic granic do przejrzenia, nie podzial.
Pokrycie liczy sie od mianownika ZRODLA: sciezka PDF bierze total_pages z
liteparse_extract.py, a split(pages, rules, total_pages=N) daje failed,
gdy parser zwrocil mniej stron, niz ma plik.
Ten skill jest warstwa PO silniku OCR, PRZED groundingiem/redakcja:
routing_gate -> (pdftotext|anydoc|opendataloader|liteparse|Chandra) -> [split_packet] -> doc-intel-contract-pl -> {gating do czlowieka | redaction_candidates | citation-grounding-pl}
Skill PRZYGOTOWUJE: kolejke review_required, liste redaction_candidates,
wspolrzedne cytatu. NIE wykonuje redakcji ani akceptacji - to robi czlowiek.
Confidence-gating to kolejka, nie werdykt prawny.
Wyjatek pozorny - mask_for_model.py. Gdy fragment ma wyjsc do modelu (rung-5
vision, LLM-sedzia), kopia dostaje maske dlugosciowa: PESEL/NIP/REGON z suma kontrolna,
IBAN, e-mail, dowod, klucze API, Bearer -> * znak w znak. Oryginal, kontrakt i bloki
sa nietkniete, wiec to NIE jest redakcja dokumentu (Article III), tylko bezpiecznik na
kanale wyjscia (Article I). Dlugosc identyczna = offsety i bbox z grounding_bridge
pasuja do oryginalu bez przeliczania.
python scripts/mask_for_model.py < fragment.txt > fragment.dla_modelu.txt
# stderr: {"zamaskowane": 3, "kategorie": ["email", "iban", "pesel"]}
mask_for_model.py.grounding_bridge.py -> zadanie citation-grounding-pl; lokalizuje cytat w blokach i doklada anchor_resolved {page,bbox,block_id} (cytat -> region).signature.py - heurystyka podpisu/pieczatki (dol strony + krotki + low-conf); detektor vision wstrzykiwalny (opt-in). Potwierdzenie wizualne = krok operatora w runtime.gaius - OCR PATRONa (Gaius-Lex /ocr/poll), engine_variant default/google_doc_ai.chandra dopasowany do REALNEGO formatu Chandry 2 (plaska lista {bbox,label,content-HTML}, 19 etykiet, BEZ confidence -> wszystko do review) + guard cichej niekompletnosci (niepuste wejscie, 0 blokow = ValueError, nie exit 0).vlm-html. Szablon promptu
references/prompt_vlm_ocr_pl.md zmusza dowolny VLM do emisji constrained HTML
z atrybutami data-label i data-bbox - wzorzec Chandry - a adapter parsuje to
do kontraktu. VLM etykietuje signature i stamp wprost, wiec podpis i
pieczatka same laduja w redaction_candidates. Do tego degeneracja.py:
detektor zapetlenia generacji, w normalize daje flage degenerate_tail
i ostrzezenie na stderr.evidence.py - rodzaj dowodu x rozdzielczosc zamiast jednej liczby, mapa
offsetow (zakres znakowy wracajacy na ORYGINALNE glify zrodla), bramki
dystynktywnosci fail-empty z jawnym match_count, gating dwuosiowy z
werdyktem trojstanowym. Do tego JEDEN DOM reguly normalizacji
(contract/normalizacja.cases.json) czytany przez oba runtime'y - Python
i citation-grounding-pl (Node), gdzie ta sama regula miala wlasna
implementacje i szesc z osmiu tych samych luk. Inspiracja: warstwa
proweniencji docling-graph (MIT/IBM) - wziete idee, nie zaleznosc.liteparse tylko w ekstraktorze.§
odzyskany 28/28, zero falszywych trafien.ok, wiec trojstan nie lapie jeszcze wszystkich wlasnych pomylek.tests/test_dokumentacja_zakotwiczona.py):
SKILL.md jest CZYTANY i konfrontowany z kodem - wersja kontraktu, enum
silnikow, obie tabele sil, kody wyjscia werdyktu, flagi CLI, sciezki plikow
i liczba testow. Dziewiec mutacji sprawdzonych na czerwono, zero luk.
Dokumentacja nie moze juz zdryfowac po cichu.Granica dowodu (stan 2026-08-05). Testy dowodza, ze parser czyta format
zgodnie ze specyfikacja - nie dowodza, ze zywy model ta specyfikacje stosuje.
Fixture vlm_html.sample.html napisalismy sami, wiec sprawdza adapter, nie
posluszenstwo VLM. Fixture chandra2.sample.json odwzorowuje format odczytany
z upstreamu chandra/output.py, ale bez przebiegu na realnej Chandrze.
Zanim silnik vlm-html pojdzie na akta, potrzebny jest przebieg bojowy:
prawdziwy skan, prawdziwy model, porownanie ze zrodlem - zgodnosc formatu
mierzy sie cudzym czytnikiem.
Render skanu do obrazow przed silnikiem VLM (flatten AcroForm + dynamiczne DPI,
pypdfium2 opcjonalnie): przepis w references/render_skanu_pl.md. UWAGA:
walidacja podpisu kwalifikowanego (skill waliduj-podpis-pdf-pl) PRZED flatten.
Most do groundingu:
python scripts/normalize.py --engine opendataloader wyjscie.json > kontrakt.json
python scripts/grounding_bridge.py kontrakt.json --quotes cytaty.txt --pretty
# -> {items:[{quote, source_text, anchor_resolved:{page,bbox,block_id,span},
# evidence, flags}], summary:{total, located_single, ambiguous,
# unlocated, gate_passed}} -> ground-citations.mjs
Governance: .matematic/konstytucja.md + .matematic/spec/001-output-contract-mvp/.
python -m unittest discover -s tests -v
Zero zaleznosci npm/pip (czysty Python 3.11+ stdlib).
Te reguły obowiązują w każdym skillu tej wtyczki, także gdy sam skill milczy. Są skopiowane do każdego skilla, więc działają zarówno po instalacji całej wtyczki, jak i pojedynczego skilla.
Plugin obsługuje dokumenty: konwersję do Markdown, redlining .docx i anonimizację danych osobowych. Część operacji dotyka danych wrażliwych, więc reguła ochrony danych jest tu pierwsza.
let-it-be), zanim treść trafi do modelu. Dane osobowe nie powinny wychodzić do API. To zasada minimalizacji (RODO).Niektóre operacje są nieodwracalne (anonimizacja w trybie nieodwracalnym, zaakceptowanie wszystkich zmian w .docx). Skille oznaczają je jawnie - wykonuj je dopiero po potwierdzeniu przez człowieka.
Wynik to projekt. Nic nie zostaje wysłane ani złożone, zanim sprawdzi to uprawniony człowiek. Plugin przygotowuje plik, nie wykonuje aktu wysyłki.
Plugin daje narzędzia na dokumentach (konwersja, redline, anonimizacja, akta i chronologia). Nie ocenia treści prawnej. Weryfikację cytatu wobec źródła prawa daje plugin „fundament weryfikacyjny”; chronologia-stanowiska-pl sprawdza tylko, czy cytat stoi na wskazanej stronie akt.