Deterministyczny weryfikator sygnatur polskich wyroków. Zero LLM w runtime. Zero halucynacji.
Sprawdzasz: czy ten wyrok istnieje?
Otrzymujesz: FOUND / NOT_FOUND / AMBIGUOUS — z twardymi faktami ze źródła.
LLM-y, które piszą o polskim prawie, konfabulują sygnatury. „Sąd Najwyższy, II CSK 999/22 — orzekł, że…" — sygnatura nie istnieje, sąd nigdy się nie wypowiedział, model brzmi pewnie.
Sententim rozwiązuje dokładnie ten jeden problem:
Zanim zacytujesz sygnaturę — sprawdź, czy ona naprawdę istnieje. Lokalnie. W mikrosekundach. Bez wysyłania niczego do chmury.
Reguła naczelna: jeśli czegoś nie ma w bazie → NOT_FOUND. Nigdy nie zgaduj.
- ❌ Nie generuje streszczeń, omówień ani interpretacji
- ❌ Nie jest alternatywą dla Lex / Legalis — to jedno małe, precyzyjne narzędzie do jednej rzeczy
- ❌ Nie korzysta z LLM w runtime — w paczce nie ma ani jednego API-call do chmury
- ❌ Nie indeksuje pełnego tekstu wyroku — FTS5 chodzi tylko po sygnaturach, sądach i podstawie prawnej
| Pole | Wartość |
|---|---|
| Wersja | sententim@0.5.x · pełen CHANGELOG |
| Domena prawna | Kredyt konsumencki + frankowy (sankcja KD, klauzule abuzywne, art. 45 ukk + 75c pr.bank) |
| Źródła | SAOS (SR/SO/SA/SN) · CELLAR / EUR-Lex (TSUE polskie wersje wyroków) |
| Korpus | 1348 wyroków · ~2011 → 2026 |
| Rozkład instancji | SO 721 · SR 459 · SA 96 · SN 57 · TSUE 15 |
| DB size | 1.23 MB · pełen lookup 0.05 – 0.4 ms |
| Toole MCP | verify_signature · search_judgments |
| Audyt | Każdy rekord: zrodlo_url + data_pobrania + sha256 + (gdy dostępne) ecli |
| Provenance | Sigstore SLSA v1 attestation na każdej publikacji npm |
claude mcp add sententim -- npx -y sententimW claude_desktop_config.json:
{
"mcpServers": {
"sententim": {
"command": "npx",
"args": ["-y", "sententim"]
}
}
}Transport: stdio, komenda: npx -y sententim.
Dlaczego
npx -y sententim(niesententim-mcp)?npx <X>szuka pakietu o nazwie<X>w npm registry, nie binarki. Pakiet nazywa sięsententim. Binarkasententimwykrywa kontekst — jeśli stdin to pipe (czyli odpala MCP klient), startuje server stdio. Jeśli stdin to TTY (interaktywny shell), drukuje help. Stary aliassententim-mcpdalej działa ponpm install -g, ale przeznpxmusisz dać pakiet jawnie:npx -y -p sententim sententim-mcp.
npm install -g sententim
claude mcp add sententim -- sententim # binarka na PATH
sententim verify "II CSK 750/15" # CLI verify
sententim info # manifest bazyimport { JudgmentsDb, runVerifySignature, runSearchJudgments } from "sententim";
const db = new JudgmentsDb(); // baza wbudowana w paczkę
// (1) weryfikacja
runVerifySignature(db, { sygnatura: "II CSK 750/15" });
// → { status: "FOUND" | "NOT_FOUND" | "AMBIGUOUS",
// matches: JudgmentMatch[], disclaimer }
// (2) wyszukiwanie
runSearchJudgments(db, { query: "apelacyjny Warszawa", limit: 5 });
// → { query, instancja: "ALL", total_returned, matches, disclaimer }
db.close();Każda publikacja na npm jest atestowana przez Sigstore — supply-chain proof, że konkretny tarball powstał z konkretnego commita w woladi/sententim przez OIDC-driven GitHub Actions:
npm audit signatures sententim
# pokaże: signatures: 1 (provenance), ✓ verifiedinput: {
sygnatura: string; // np. "II CSK 750/15"
sad?: string; // zawęża po substringu nazwy sądu
data?: string; // zawęża po dokładnej dacie ISO YYYY-MM-DD
}
output: {
status: "FOUND" | "NOT_FOUND" | "AMBIGUOUS",
matches: JudgmentMatch[],
disclaimer: "Dane deterministyczne ze źródła publicznego. Zweryfikuj treść w źródle. Nie stanowi porady prawnej."
}| status | znaczenie | matches |
|---|---|---|
FOUND |
dokładnie jedno trafienie | [{...}] |
NOT_FOUND |
zero trafień — nie cytuj tej sygnatury | [] |
AMBIGUOUS |
ta sama sygnatura w ≥2 sądach — zwracamy wszystkich kandydatów, bez wybierania | [{...}, {...}, …] |
FTS5 po sygnaturze, nazwie sądu i podstawie prawnej. Akcento-niewrażliwa (unicode61 remove_diacritics=2), naiwna na polską morfologię (Warszawa znajduje Warszawie przez stem trimming).
input: {
query: string; // multi-token AND, np. "apelacyjny Warszawa"
instancja?: "SR"|"SO"|"SA"|"SN"|"NSA"|"WSA"|"TK"|"TSUE";
limit?: number; // 1-50, default 10
}
output: {
query: string,
instancja: "SR"|"SO"|"SA"|"SN"|"NSA"|"WSA"|"TK"|"TSUE"|"ALL",
total_returned: number,
matches: JudgmentMatch[],
disclaimer: string
}Ważne: w search wciąż obowiązuje reguła naczelna. total_returned: 0 ≠ "wyrok nie istnieje" — to "nie ma w tej bazie". Nie cytuj na podstawie braku trafień.
type Judgment = {
sygnatura: string; // "II CSK 750/15"
sygnatura_norm: string; // matchowanie: upper-case, bez kropek, ASCII
sad: string; // "Sąd Rejonowy w Olsztynie"
instancja: "SR"|"SO"|"SA"|"SN"|"NSA"|"WSA"|"TK"|"TSUE";
data_orzeczenia: string; // ISO YYYY-MM-DD
sentencja_typ: "oddala"|"uwzglednia"|"uchyla_przekazuje"|"zmienia"|"umarza"|"inne"|null;
prawomocny: 0|1|null; // SA/SN/NSA/TK/TSUE → 1 by construction; SR/SO via cross-ref
uchylony_przez: string|null; // backfilled przez cross-ref pass (rzadko na narrow corpus)
podstawa_prawna: string[]; // ["art. 45 ukk", "art. 75c pr.bank"]
zrodlo_url: string;
data_pobrania: string; // ISO timestamp
sha256: string; // hash surowego textContent (audyt)
};JudgmentMatch (zwracany przez oba toole MCP) to projekcja Judgment bez sygnatura_norm i sha256 — pola audytowe nie wyciekają przez MCP.
Każde pole pochodzi z deterministycznej ekstrakcji — pełna lista parserów: scripts/etl/parsers/.
(one-shot, ~15-20 min, lokalnie u devy)
SAOS REST API ──── seed.ts ─────┐
│
▼
data/judgments.db ← committed do repo,
(~1300 wierszy, FTS5) shipped w paczce npm
│
│ better-sqlite3 (sync, PRAGMA query_only=1)
▼
sententim-mcp · stdio ← runtime: 0 LLM, 0 sieci
│
┌───────┴────────┐
▼ ▼
verify_signature search_judgments
│ │
▼ ▼
FOUND/NOT_FOUND/AMBIGUOUS/OUT_OF_SCOPE ranked matches
| Komponent | Stack |
|---|---|
| Schema | SQLite + FTS5 (tokenize="unicode61 remove_diacritics 2") |
| Runtime | better-sqlite3 (sync), prepared statements, PRAGMA query_only=1 |
| MCP | @modelcontextprotocol/sdk 1.x, stdio transport |
| ETL | TypeScript pure-fn — zero LLM, zero zewnętrznych API poza SAOS |
| CI/CD | GitHub Actions · npm Trusted Publishing (OIDC) · Sigstore provenance |
- SAOS-owy SN zamrożony na 2016-06-22 (CeON przestał ingestować z sn.pl). W korpusie mamy 57 wyroków SN do tej daty z domeny kredytu konsumenckiego / klauzul abuzywnych. Roadmap v0.6: scraper sn.pl dla post-2016 SN.
- TSUE — pokryte hand-curated listą 15 najważniejszych orzeczeń dla domeny (consumer credit Directive 2008/48 + klauzule abuzywne D.93/13). Roadmap v0.6: rozszerzyć przez CELLAR SPARQL + EUROVOC concept code, gdy zweryfikujemy go dla "kredyt konsumencki".
prawomocny: SA/SN/NSA/TK/TSUE →1z definicji; SR/SO →1tylko gdy w korpusie istnieje appellate zesentencja_typ=oddalareferujące tę sygnaturę; inaczejNULL. Na korpusie v0.5 (1348 rekordów) → 169 by-instance + 19 by-cross-ref = 188 prawomocnych, 1160 NULL.uchylony_przez— backfilluje cross-ref pass na podstawie wzorca "sygn. akt X" w textContent appellate'ówuchyla_przekazuje. Na korpusie v0.5 = 4 trafienia (z 0 w v0.4 dzięki rozszerzeniu o SN-owe powołania).sentencja_typNULL~31% — świadomie zamiast zgadywać'inne'. Najczęściej compound rulings ("uchyla w części, w pozostałej oddala") które wymagają mocniejszej heurystyki.search_judgments— stem-aware ale nie pełna morfologia.Warszawa→Warszaw*łapie "Warszawie/Warszawy", ale rzadkie odmiany mogą umknąć.search_judgmentsnie szuka po pełnym tekście wyroku — FTS5 indeksuje tylko(sygnatura, sygnatura_norm, podstawa_prawna, sad). Pełen tekst nie ląduje w bazie (sha256 jako audyt). Pytanie typu "RODO" znajdzie tylko gdy "RODO" jest wpodstawa_prawna.- TSUE
data_orzeczeniato rok-styczeń-1 — CELLAR HTML nie udostępnia daty w machine-readable miejscu, więc stamping naYYYY-01-01(gdzieYYYYto rok z CELEX-u). Realna data wymaga parsowania HTML body — roadmap v0.6. - Daty filtrowane do zakresu
1990-01-01 ... dzisiaj+1d(literówki w źródle typu „3013-…" są odrzucane).
- v0.1 —
verify_signature, sankcja kredytu darmowego, SAOS COMMON. - v0.2 —
search_judgments(FTS5), prawomocny heurystyka + cross-ref pass dla SR/SO, OIDC Trusted Publishing. - v0.3 —
npx -y sententimsmart-entry (binarka detect MCP/CLI). - v0.4 — Nowy status
OUT_OF_SCOPE+ heurystyka klasyfikacji wzorca SN/TSUE/NSA/TK, sanitiser FTS5 / rok 4-cyfrowy / stem-aware filtrsad. - v0.5 — SN (SAOS-owy, do 2016-06-22) + TSUE (curated 15 spraw). Schema: opcjonalne
ecli. Korpus 1348 wyroków · ["SR","SO","SA","SN","TSUE"]. Tu jesteś. - v0.6 — Scraper sn.pl dla post-2016 SN + CELLAR SPARQL+EUROVOC do automatycznego rozszerzania TSUE + real
data_orzeczeniaz TSUE HTML.
Streszczenia LLM — tylko z human-in-the-loop i flagą provenance, nigdy w default-path.
pnpm install # patrz: native build niżej
pnpm typecheck
pnpm lint
pnpm test # 66 testów, wszystko in-memory (bez sieci)
pnpm etl:seed # ~15-20 min, lokalnie, produkuje data/judgments.db
pnpm etl:seed --max=50 # smoke (~30s, nie produkuje ship-grade DB)
pnpm etl:seed --skip-fetch # rebuild DB z istniejących raw JSONL
pnpm etl:verify # pre-publish gate (PRAGMA query_only + latencja)
pnpm build # → dist/ (ESM + .d.ts + maps)pnpm 11 blokuje domyślnie buildy natywne. Przy pierwszym pnpm install może pojawić się ostrzeżenie ERR_PNPM_IGNORED_BUILDS. Jednorazowy fix:
pnpm install --config.dangerouslyAllowAllBuilds=true
# albo
pnpm approve-builds # interaktywnie zaakceptuj: better-sqlite3, esbuild, @biomejs/biomePo skompilowaniu .node binary (~25s na Apple Silicon), kolejne pnpm install już nie wymagają flagi. To samo dotyczy CI — workflow'y używają --config.dangerouslyAllowAllBuilds=true.
Każdy wiersz w bazie ma zrodlo_url + data_pobrania + sha256(textContent). Weryfikacja, że nasze pola pochodzą z tych bajtów:
# Bierzemy losowy rekord:
node -e "const D=require('better-sqlite3'); const db=new D('data/judgments.db',{readonly:true}); console.log(db.prepare('SELECT zrodlo_url, sha256 FROM judgments ORDER BY RANDOM() LIMIT 1').get())"
# Pobieramy źródło, hashujemy textContent, porównujemy:
curl -s "<zrodlo_url>" | jq -r .textContent | shasum -a 256Publikacja na npm leci automatycznie z GitHub Actions, bez NPM_TOKEN.
- Dodaj
.changeset/<opis>.md(lubpnpm changeset):--- "sententim": patch | minor | major --- Krótki opis zmiany.
git push origin main→release.ymlotwiera (lub aktualizuje) PR "Version Packages" który bumpujepackage.json+ dopisuje wpis doCHANGELOG.md.- Merge PR →
release.ymlponownie się odpala. Tym razem nie ma pending changesets, więcchangesets/actionwołapnpm release(= changeset publish= npm publish). npm CLI wykrywa OIDC w runnerze (id-token: write+actions/setup-nodezregistry-url), wymienia token GitHub na krótkotrwały token publikacji npm i wysyła tarball z Sigstore provenance (włączone przezpublishConfig.provenance: true). - Bez sekretów
NPM_TOKEN. Bez ręcznegonpm login. Atestacja widoczna na stronie pakietu w npm.
Pre-req jednorazowy (web UI): konfiguracja Trusted Publishera na npmjs.com pod Owner=woladi · Repo=sententim · Workflow=release.yml.
sententim/
├── src/
│ ├── index.ts · stdio MCP entry + public API exports
│ ├── server.ts · MCP server (verify_signature + search_judgments)
│ ├── db.ts · JudgmentsDb (PRAGMA query_only=1, FTS5)
│ ├── normalize.ts · displaySignature + normaliseSignature
│ ├── types.ts · Judgment, VerifyResult, Manifest, JudgmentMatch
│ ├── cli.ts · `sententim info` / `sententim verify`
│ └── tools/
│ ├── verify-signature.ts · FOUND / NOT_FOUND / AMBIGUOUS
│ └── search-judgments.ts · FTS5-backed search
├── scripts/etl/
│ ├── parsers/
│ │ ├── sentencja-typ.ts · regex outcome classifier
│ │ ├── podstawa-prawna.ts · regex legal-basis extractor
│ │ ├── sad-instancja.ts · court → SR/SO/SA/SN/...
│ │ └── cross-ref.ts · sygn. akt X regex (prawomocny + uchylony_przez)
│ ├── sources/
│ │ ├── saos.ts · SAOS REST API client
│ │ └── cjeu.ts · gated SENTENTIM_ENABLE_CJEU (deferred)
│ ├── normalize.ts · raw SAOS → canonical + cross-ref pass
│ ├── build-db.ts · staged JSONL → SQLite + manifest
│ ├── seed.ts · unia 2 queries SAOS
│ └── verify.ts · prepublish gate
├── data/
│ ├── schema.sql · DDL (committed)
│ ├── judgments.db · published artefact
│ └── manifest.json · published artefact
├── tests/ · 66 vitest tests
│ ├── normalize.test.ts
│ ├── parsers.test.ts
│ ├── cross-ref.test.ts
│ ├── db.test.ts
│ ├── verify-contract.test.ts
│ └── search-judgments.test.ts
└── .github/workflows/
├── ci.yml · typecheck · lint · test · build
└── release.yml · changesets → OIDC → npm + Sigstore