https://github.com/woladi/sententim
Deterministyczny weryfikator sygnatur polskich wyroków · MCP · lokalnie · offline · zero LLM w runtime. FOUND / NOT_FOUND / AMBIGUOUS — z twardymi faktami ze źródła publicznego (SAOS). Sub-10ms lookup.
https://github.com/woladi/sententim
ai claude deterministic legaltech llm local-first mcp model-context-protocol no-llm-runtime orzecznictwo polish-law sad-najwyzszy saos sqlite sygnatura typescript
Last synced: 15 days ago
JSON representation
Deterministyczny weryfikator sygnatur polskich wyroków · MCP · lokalnie · offline · zero LLM w runtime. FOUND / NOT_FOUND / AMBIGUOUS — z twardymi faktami ze źródła publicznego (SAOS). Sub-10ms lookup.
- Host: GitHub
- URL: https://github.com/woladi/sententim
- Owner: woladi
- License: other
- Created: 2026-06-02T09:03:52.000Z (24 days ago)
- Default Branch: main
- Last Pushed: 2026-06-02T10:45:56.000Z (24 days ago)
- Last Synced: 2026-06-02T12:15:48.370Z (24 days ago)
- Topics: ai, claude, deterministic, legaltech, llm, local-first, mcp, model-context-protocol, no-llm-runtime, orzecznictwo, polish-law, sad-najwyzszy, saos, sqlite, sygnatura, typescript
- Language: TypeScript
- Homepage: https://github.com/woladi/sententim
- Size: 447 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# sententim
**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.
[](https://www.npmjs.com/package/sententim)
[](https://www.npmjs.com/package/sententim)
[](LICENSE)
[](package.json)
[](https://github.com/woladi/sententim/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/sententim)
[](https://modelcontextprotocol.io)
---
## Po co to istnieje
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.**
## Czym to NIE jest
- ❌ 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
## Co jest w paczce
| Pole | Wartość |
|---|---|
| **Wersja** | sententim@0.5.x · [pełen CHANGELOG](CHANGELOG.md) |
| **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 |
## Quick start
### Claude Code (CLI)
```bash
claude mcp add sententim -- npx -y sententim
```
### Claude Desktop
W `claude_desktop_config.json`:
```json
{
"mcpServers": {
"sententim": {
"command": "npx",
"args": ["-y", "sententim"]
}
}
}
```
### Cursor / Continue / inny klient MCP
Transport: stdio, komenda: `npx -y sententim`.
> **Dlaczego `npx -y sententim` (nie `sententim-mcp`)?** `npx ` szuka **pakietu** o nazwie `` w npm registry, nie binarki. Pakiet nazywa się `sententim`. Binarka `sententim` wykrywa kontekst — jeśli stdin to pipe (czyli odpala MCP klient), startuje server stdio. Jeśli stdin to TTY (interaktywny shell), drukuje help. Stary alias `sententim-mcp` dalej działa po `npm install -g`, ale przez `npx` musisz dać pakiet jawnie: `npx -y -p sententim sententim-mcp`.
### Global install (alternatywa, jednorazowa)
```bash
npm install -g sententim
claude mcp add sententim -- sententim # binarka na PATH
sententim verify "II CSK 750/15" # CLI verify
sententim info # manifest bazy
```
### Z poziomu kodu
```ts
import { 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();
```
### Weryfikacja Sigstore provenance
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:
```bash
npm audit signatures sententim
# pokaże: signatures: 1 (provenance), ✓ verified
```
---
## Kontrakty narzędzi MCP
### Tool: `verify_signature`
```ts
input: {
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** | `[{...}, {...}, …]` |
### Tool: `search_judgments`
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).
```ts
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ń.
## Schemat danych
```ts
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/`.
## Architektura
```
(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 |
## Limity i znane luki
- **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 → `1` z definicji; SR/SO → `1` tylko gdy w korpusie istnieje appellate ze `sentencja_typ=oddala` referujące tę sygnaturę; inaczej `NULL`. 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'ów `uchyla_przekazuje`. Na korpusie v0.5 = **4 trafienia** (z 0 w v0.4 dzięki rozszerzeniu o SN-owe powołania).
- **`sentencja_typ` `NULL` ~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_judgments` nie 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 w `podstawa_prawna`.
- **TSUE `data_orzeczenia` to rok-styczeń-1** — CELLAR HTML nie udostępnia daty w machine-readable miejscu, więc stamping na `YYYY-01-01` (gdzie `YYYY` to 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).
## Roadmap
- **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 sententim` smart-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 filtr `sad`.
- **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_orzeczenia` z TSUE HTML.
Streszczenia LLM — **tylko z human-in-the-loop i flagą provenance**, **nigdy** w default-path.
## Development
```bash
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)
```
### Native build (better-sqlite3)
`pnpm 11` blokuje domyślnie buildy natywne. Przy pierwszym `pnpm install` może pojawić się ostrzeżenie `ERR_PNPM_IGNORED_BUILDS`. Jednorazowy fix:
```bash
pnpm install --config.dangerouslyAllowAllBuilds=true
# albo
pnpm approve-builds # interaktywnie zaakceptuj: better-sqlite3, esbuild, @biomejs/biome
```
Po 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`.
### Audyt determinizmu
Każdy wiersz w bazie ma `zrodlo_url` + `data_pobrania` + `sha256(textContent)`. Weryfikacja, że nasze pola pochodzą z tych bajtów:
```bash
# 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 "" | jq -r .textContent | shasum -a 256
```
### Release (OIDC Trusted Publishing)
Publikacja na npm leci automatycznie z GitHub Actions, **bez `NPM_TOKEN`**.
1. Dodaj `.changeset/.md` (lub `pnpm changeset`):
```md
---
"sententim": patch | minor | major
---
Krótki opis zmiany.
```
2. `git push origin main` → `release.yml` otwiera (lub aktualizuje) PR **"Version Packages"** który bumpuje `package.json` + dopisuje wpis do `CHANGELOG.md`.
3. Merge PR → `release.yml` ponownie się odpala. Tym razem nie ma pending changesets, więc `changesets/action` woła `pnpm release` (`= changeset publish` `= npm publish`). npm CLI wykrywa OIDC w runnerze (`id-token: write` + `actions/setup-node` z `registry-url`), wymienia token GitHub na krótkotrwały token publikacji npm i wysyła tarball z **Sigstore provenance** (włączone przez `publishConfig.provenance: true`).
4. Bez sekretów `NPM_TOKEN`. Bez ręcznego `npm 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`.
### Layout
```
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
```
## License
[MIT](LICENSE) © Adrian Wołczuk. Data-source attribution: [NOTICE](NOTICE).