T1: Deduplicator compile fix + unit tests [checkpoint]

This commit is contained in:
Artur Kruszewski
2026-07-07 22:20:08 +02:00
commit 38a93d0c4d
114 changed files with 6942 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# cavecrew
Decision guide. When to delegate to caveman subagents instead of doing the work inline.
## What it does
Tells the main thread when to spawn a caveman-style subagent versus the vanilla equivalent. The win: subagent tool-results inject back into main context verbatim, and caveman output is roughly 1/3 the size of vanilla prose. Across 20 delegations in one session, that is the difference between context exhaustion and finishing the task.
Three subagents:
| Subagent | Job | Use when |
|----------|-----|----------|
| `cavecrew-investigator` | Locate code (read-only) | "Where is X defined / what calls Y / list uses of Z" |
| `cavecrew-builder` | Surgical edit, 1-2 files | Scope is obvious, ≤2 files. Refuses 3+ file scope. |
| `cavecrew-reviewer` | Diff/file review | One-line findings with severity emoji |
Use vanilla `Explore` or `Code Reviewer` when you want prose, architecture commentary, or rationale. Use main thread directly for one-line answers and 3+ file refactors.
This skill is a decision guide, not a slash command. It activates when the conversation mentions delegation.
## How to invoke
Triggers on phrases like "delegate to subagent", "use cavecrew", "spawn investigator", "save context", "compressed agent output".
## Example chaining
Locate → fix → verify (most common):
1. `cavecrew-investigator` returns site list (`path:line — symbol — note`)
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`
3. `cavecrew-reviewer` audits the resulting diff
Parallel scout: spawn 2-3 `cavecrew-investigator` calls in one message with different angles (defs, callers, tests). Aggregate in main.
## Model overrides
By default, `cavecrew-reviewer` and `cavecrew-investigator` pin `model: haiku` in their frontmatter; `cavecrew-builder` has no `model:` line (uses the API session default). Set env vars in your shell before launching Claude Code to override per-agent:
| Env var | Agent |
|---|---|
| `CAVECREW_REVIEWER_MODEL` | `cavecrew-reviewer` |
| `CAVECREW_BUILDER_MODEL` | `cavecrew-builder` |
| `CAVECREW_INVESTIGATOR_MODEL` | `cavecrew-investigator` |
Example — run reviewer on sonnet, keep others on default:
```sh
export CAVECREW_REVIEWER_MODEL=sonnet
```
Use the same model name strings you'd use in any Claude Code agent frontmatter (e.g. `haiku`, `sonnet`, `opus`).
Overrides patch only the `model:` line in the installed agent's frontmatter; the prompt body is untouched and keeps receiving upstream updates. Plugin installs only — standalone hook installs have no local agent files to patch. Unset or blank = no change. The patch persists in the installed file until the plugin is updated or reinstalled.
## See also
- [`SKILL.md`](./SKILL.md) — full decision matrix and output contracts
- [`agents/cavecrew-investigator.md`](../../agents/cavecrew-investigator.md)
- [`agents/cavecrew-builder.md`](../../agents/cavecrew-builder.md)
- [`agents/cavecrew-reviewer.md`](../../agents/cavecrew-reviewer.md)
- [Caveman README](../../README.md) — repo overview
+82
View File
@@ -0,0 +1,82 @@
---
name: cavecrew
description: >
Decision guide for delegating to caveman-style subagents. Tells the main
thread WHEN to spawn `cavecrew-investigator` (locate code), `cavecrew-builder`
(1-2 file edit), or `cavecrew-reviewer` (diff review) instead of doing the
work inline or using vanilla `Explore`. Subagent output is caveman-compressed
so the tool-result injected back into main context is ~60% smaller — main
context lasts longer across long sessions.
Trigger: "delegate to subagent", "use cavecrew", "spawn investigator/builder/reviewer",
"save context", "compressed agent output".
---
Cavecrew = three subagent presets that emit caveman output. Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.
## When to use cavecrew vs alternatives
| Task | Use |
|---|---|
| "Where is X defined / what calls Y / list uses of Z" | `cavecrew-investigator` |
| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |
| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |
| New feature / 3+ files / cross-cutting refactor | Main thread or `feature-dev:code-architect` |
| Review diff, branch, or file for bugs | `cavecrew-reviewer` |
| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) |
| One-line answer you already know | Main thread, no subagent |
Rule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**
## Why this exists (the real win)
Subagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` returns ~700 tokens. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.
## Output contracts
What main thread can rely on per agent:
**`cavecrew-investigator`**
```
<Header>:
- path:line — `symbol` — short note
totals: <counts>.
```
Or `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\d+`.
**`cavecrew-builder`**
```
<path:line-range> — <change ≤10 words>.
verified: <re-read OK | mismatch @ path:line>.
```
Or one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).
**`cavecrew-reviewer`**
```
path:line: <emoji> <severity>: <problem>. <fix>.
totals: N🔴 N🟡 N🔵 N❓
```
Or `No issues.` Findings sorted file → line ascending.
## Chaining patterns
**Locate → fix → verify** (most common):
1. `cavecrew-investigator` returns site list.
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.
3. `cavecrew-reviewer` audits the diff.
**Parallel scout** (when investigation is broad):
Spawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.
**Single-shot edit** (when site is already known):
Skip investigator. Hand exact path:line to `cavecrew-builder` directly.
## What NOT to do
- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.
- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.
- Don't ask `cavecrew-reviewer` for "general feedback" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that.
- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.
## Auto-clarity (inherited)
Subagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.
+44
View File
@@ -0,0 +1,44 @@
# caveman-commit
Terse Conventional Commits. Why over what.
## What it does
Generates commit messages in Conventional Commits format. Subject ≤50 chars, hard cap 72. Imperative mood. Body only when the *why* is non-obvious or there are breaking changes. No AI attribution, no "this commit does X", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts — future debuggers need the context.
Outputs only the message. Does not stage, commit, or amend.
## How to invoke
```
/caveman-commit
```
Also triggers on phrases like "write a commit", "commit message", "generate commit".
## Example output
Diff: new endpoint for user profile.
```
feat(api): add GET /users/:id/profile
Mobile client needs profile data without the full user payload
to reduce LTE bandwidth on cold-launch screens.
Closes #128
```
Diff: breaking API rename.
```
feat(api)!: rename /v1/orders to /v1/checkout
BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
before 2026-06-01. Old route returns 410 after that date.
```
## See also
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
- [Caveman README](../../README.md) — repo overview
+65
View File
@@ -0,0 +1,65 @@
---
name: caveman-commit
description: >
Ultra-compressed commit message generator. Cuts noise from commit messages while preserving
intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when "why"
isn't obvious. Use when user says "write a commit", "commit message", "generate commit",
"/commit", or invokes /caveman-commit. Auto-triggers when staging changes.
---
Write commit messages terse and exact. Conventional Commits format. No fluff. Why over what.
## Rules
**Subject line:**
- `<type>(<scope>): <imperative summary>``<scope>` optional
- Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert`
- Imperative mood: "add", "fix", "remove" — not "added", "adds", "adding"
- ≤50 chars when possible, hard cap 72
- No trailing period
- Match project convention for capitalization after the colon
**Body (only if needed):**
- Skip entirely when subject is self-explanatory
- Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues
- Wrap at 72 chars
- Bullets `-` not `*`
- Reference issues/PRs at end: `Closes #42`, `Refs #17`
**What NEVER goes in:**
- "This commit does X", "I", "we", "now", "currently" — the diff says what
- "As requested by..." — use Co-authored-by trailer
- "Generated with Claude Code" or any AI attribution — unless the user's own rule requires an `Assisted-by`/AI-attribution trailer, then add it as a trailer
- Emoji (unless project convention requires)
- Restating the file name when scope already says it
## Examples
Diff: new endpoint for user profile with body explaining the why
- ❌ "feat: add a new endpoint to get user profile information from the database"
-
```
feat(api): add GET /users/:id/profile
Mobile client needs profile data without the full user payload
to reduce LTE bandwidth on cold-launch screens.
Closes #128
```
Diff: breaking API change
- ✅
```
feat(api)!: rename /v1/orders to /v1/checkout
BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
before 2026-06-01. Old route returns 410 after that date.
```
## Auto-Clarity
Always include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context.
## Boundaries
Only generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. "stop caveman-commit" or "normal mode": revert to verbose commit style.
+163
View File
@@ -0,0 +1,163 @@
<p align="center">
<img src="https://em-content.zobj.net/source/apple/391/rock_1faa8.png" width="80" />
</p>
<h1 align="center">caveman-compress</h1>
<p align="center">
<strong>shrink memory file. save token every session.</strong>
</p>
---
A Claude Code skill that compresses your project memory files (`CLAUDE.md`, todos, preferences) into caveman format — so every session loads fewer tokens automatically.
Claude read `CLAUDE.md` on every session start. If file big, cost big. Caveman make file small. Cost go down forever.
## What It Do
```
/caveman-compress CLAUDE.md
```
```
CLAUDE.md ← compressed (Claude reads this — fewer tokens every session)
CLAUDE.original.md ← human-readable backup (you edit this)
```
Original never lost. You can read and edit `.original.md`. Run skill again to re-compress after edits.
## Benchmarks
Real results on real project files:
| File | Original | Compressed | Saved |
|------|----------:|----------:|------:|
| `claude-md-preferences.md` | 706 | 285 | **59.6%** |
| `project-notes.md` | 1145 | 535 | **53.3%** |
| `claude-md-project.md` | 1122 | 636 | **43.3%** |
| `todo-list.md` | 627 | 388 | **38.1%** |
| `mixed-with-code.md` | 888 | 560 | **36.9%** |
| **Average** | **898** | **481** | **46%** |
All validations passed ✅ — headings, code blocks, URLs, file paths preserved exactly.
## Before / After
<table>
<tr>
<td width="50%">
### 📄 Original (706 tokens)
> "I strongly prefer TypeScript with strict mode enabled for all new code. Please don't use `any` type unless there's genuinely no way around it, and if you do, leave a comment explaining the reasoning. I find that taking the time to properly type things catches a lot of bugs before they ever make it to runtime."
</td>
<td width="50%">
### <img src="../../docs/assets/dancing-rock.svg" width="20" height="20" alt="rock"/> Caveman (285 tokens)
> "Prefer TypeScript strict mode always. No `any` unless unavoidable — comment why if used. Proper types catch bugs early."
</td>
</tr>
</table>
**Same instructions. 60% fewer tokens. Every. Single. Session.**
## Security
`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O patterns detected by static analysis. This is a false positive — see [SECURITY.md](./SECURITY.md) for a full explanation of what the skill does and does not do.
## Install
Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman-compress`.
If you need local files, the compress skill lives at:
```bash
caveman-compress/
```
**Requires:** Python 3.10+
## Usage
```
/caveman-compress <filepath>
```
Examples:
```
/caveman-compress CLAUDE.md
/caveman-compress docs/preferences.md
/caveman-compress todos.md
```
### What files work
| Type | Compress? |
|------|-----------|
| `.md`, `.txt`, `.rst`, `.typ`, `.typst`, `.tex` | ✅ Yes |
| Extensionless natural language | ✅ Yes |
| `.py`, `.js`, `.ts`, `.json`, `.yaml` | ❌ Skip (code/config) |
| `*.original.md` | ❌ Skip (backup files) |
## How It Work
```
/caveman-compress CLAUDE.md
detect file type (no tokens)
Claude compresses (tokens — one call)
validate output (no tokens)
checks: headings, code blocks, URLs, file paths, bullets
if errors: Claude fixes cherry-picked issues only (tokens — targeted fix)
does NOT recompress — only patches broken parts
retry up to 2 times
write compressed → CLAUDE.md
write original → CLAUDE.original.md
```
Only two things use tokens: initial compression + targeted fix if validation fails. Everything else is local Python.
## What Is Preserved
Caveman compress natural language. It never touch:
- Code blocks (` ``` ` fenced or indented)
- Inline code (`` `backtick content` ``)
- URLs and links
- File paths (`/src/components/...`)
- Commands (`npm install`, `git commit`)
- Technical terms, library names, API names
- Headings (exact text preserved)
- Tables (structure preserved, cell text compressed)
- Dates, version numbers, numeric values
## Why This Matter
`CLAUDE.md` loads on **every session start**. A 1000-token project memory file costs tokens every single time you open a project. Over 100 sessions that's 100,000 tokens of overhead — just for context you already wrote.
Caveman cut that by ~46% on average. Same instructions. Same accuracy. Less waste.
```
┌────────────────────────────────────────────┐
│ TOKEN SAVINGS PER FILE █████ 46% │
│ SESSIONS THAT BENEFIT ██████████ 100% │
│ INFORMATION PRESERVED ██████████ 100% │
│ SETUP TIME █ 1x │
└────────────────────────────────────────────┘
```
## Part of Caveman
This skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit — making Claude use fewer tokens without losing accuracy.
- **caveman** — make Claude *speak* like caveman (cuts response tokens ~65%)
- **caveman-compress** — make Claude *read* less (cuts context tokens ~46%)
@@ -0,0 +1,31 @@
# Security
## Snyk High Risk Rating
`caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do.
### What triggers the rating
1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. The subprocess call uses a fixed argument list — no shell interpolation occurs. User file content is passed via stdin, not as a shell argument.
2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved alongside it. No files outside the user-specified path are read or written.
### What the skill does NOT do
- Does not execute user file content as code
- Does not make network requests except to Anthropic's API (via SDK or CLI)
- Does not access files outside the path the user provides
- Does not use shell=True or string interpolation in subprocess calls
- Does not collect or transmit any data beyond the file being compressed
### Auth behavior
If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication.
### File size limit
Files larger than 500KB are rejected before any API call is made.
### Reporting a vulnerability
If you believe you've found a genuine security issue, please open a GitHub issue with the label `security`.
+111
View File
@@ -0,0 +1,111 @@
---
name: caveman-compress
description: >
Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format
to save input tokens. Preserves all technical substance, code, URLs, and structure.
Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md.
Trigger: /caveman-compress FILEPATH or "compress memory file"
---
# Caveman Compress
## Purpose
Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`.
## Trigger
`/caveman-compress <filepath>` or when user asks to compress a memory file.
## Process
1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.
2. From the directory containing this SKILL.md, run:
python3 -m scripts <absolute_filepath>
3. The CLI will:
- detect file type (no tokens)
- call Claude to compress
- validate output (no tokens)
- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)
- retry up to 2 times
- if still failing after 2 retries: report error to user, leave original file untouched
4. Return result to user
## Compression Rules
### Remove
- Articles: a, an, the
- Filler: just, really, basically, actually, simply, essentially, generally
- Pleasantries: "sure", "certainly", "of course", "happy to", "I'd recommend"
- Hedging: "it might be worth", "you could consider", "it would be good to"
- Redundant phrasing: "in order to" → "to", "make sure to" → "ensure", "the reason is because" → "because"
- Connective fluff: "however", "furthermore", "additionally", "in addition"
### Preserve EXACTLY (never modify)
- Code blocks (fenced ``` and indented)
- Inline code (`backtick content`)
- URLs and links (full URLs, markdown links)
- File paths (`/src/components/...`, `./config.yaml`)
- Commands (`npm install`, `git commit`, `docker build`)
- Technical terms (library names, API names, protocols, algorithms)
- Proper nouns (project names, people, companies)
- Dates, version numbers, numeric values
- Environment variables (`$HOME`, `NODE_ENV`)
### Preserve Structure
- All markdown headings (keep exact heading text, compress body below)
- Bullet point hierarchy (keep nesting level)
- Numbered lists (keep numbering)
- Tables (compress cell text, keep structure)
- Frontmatter/YAML headers in markdown files
### Compress
- Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize"
- Fragments OK: "Run tests before commit" not "You should always run tests before committing"
- Drop "you should", "make sure to", "remember to" — just state the action
- Merge redundant bullets that say the same thing differently
- Keep one example where multiple examples show the same pattern
CRITICAL RULE:
Anything inside ``` ... ``` must be copied EXACTLY.
Do not:
- remove comments
- remove spacing
- reorder lines
- shorten commands
- simplify anything
Inline code (`...`) must be preserved EXACTLY.
Do not modify anything inside backticks.
If file contains code blocks:
- Treat code blocks as read-only regions
- Only compress text outside them
- Do not merge sections around code
## Pattern
Original:
> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.
Compressed:
> Run tests before push to main. Catch bugs early, prevent broken prod deploys.
Original:
> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.
Compressed:
> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.
## Boundaries
- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)
- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh
- If file has mixed content (prose + code), compress ONLY the prose sections
- If unsure whether something is code or prose, leave it unchanged
- Original file is backed up as FILE.original.md before overwriting
- Never compress FILE.original.md (skip it)
@@ -0,0 +1,9 @@
"""Caveman compress scripts.
This package provides tools to compress natural language markdown files
into caveman format to save input tokens.
"""
__all__ = ["cli", "compress", "detect", "validate"]
__version__ = "1.0.0"
@@ -0,0 +1,3 @@
from .cli import main
main()
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
# Support both direct execution and module import
try:
from .validate import validate
except ImportError:
sys.path.insert(0, str(Path(__file__).parent))
from validate import validate
try:
import tiktoken
_enc = tiktoken.get_encoding("o200k_base")
except ImportError:
_enc = None
def count_tokens(text):
if _enc is None:
return len(text.split()) # fallback: word count
return len(_enc.encode(text))
def benchmark_pair(orig_path: Path, comp_path: Path):
orig_text = orig_path.read_text()
comp_text = comp_path.read_text()
orig_tokens = count_tokens(orig_text)
comp_tokens = count_tokens(comp_text)
saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0
result = validate(orig_path, comp_path)
return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid)
def print_table(rows):
print("\n| File | Original | Compressed | Saved % | Valid |")
print("|------|----------|------------|---------|-------|")
for r in rows:
print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]:.1f}% | {'' if r[4] else ''} |")
def main():
# Direct file pair: python3 benchmark.py original.md compressed.md
if len(sys.argv) == 3:
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
if not orig.exists():
print(f"❌ Not found: {orig}")
sys.exit(1)
if not comp.exists():
print(f"❌ Not found: {comp}")
sys.exit(1)
print_table([benchmark_pair(orig, comp)])
return
# Glob mode: repo_root/tests/caveman-compress/
# __file__ lives at <repo_root>/skills/caveman-compress/scripts/benchmark.py
# Walk up four dirs: scripts → caveman-compress → skills → repo_root.
tests_dir = Path(__file__).resolve().parents[3] / "tests" / "caveman-compress"
if not tests_dir.exists():
print(f"❌ Tests dir not found: {tests_dir}")
sys.exit(1)
rows = []
for orig in sorted(tests_dir.glob("*.original.md")):
comp = orig.with_name(orig.stem.removesuffix(".original") + ".md")
if comp.exists():
rows.append(benchmark_pair(orig, comp))
if not rows:
print("No compressed file pairs found.")
return
print_table(rows)
if __name__ == "__main__":
main()
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Caveman Compress CLI
Usage:
caveman <filepath>
"""
import sys
# Force UTF-8 on stdout/stderr before any code can print. Windows consoles
# default to cp1252 and crash on the ❌ glyphs in error/validation branches,
# masking the real error and leaving the user with a half-compressed file.
for _stream in (sys.stdout, sys.stderr):
reconfigure = getattr(_stream, "reconfigure", None)
if callable(reconfigure):
try:
reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
from pathlib import Path
from .compress import backup_dir_for, compress_file
from .detect import detect_file_type, should_compress
def print_usage():
print("Usage: caveman <filepath>")
def main():
if len(sys.argv) != 2:
print_usage()
sys.exit(1)
filepath = Path(sys.argv[1])
# Check file exists
if not filepath.exists():
print(f"❌ File not found: {filepath}")
sys.exit(1)
if not filepath.is_file():
print(f"❌ Not a file: {filepath}")
sys.exit(1)
filepath = filepath.resolve()
# Detect file type
file_type = detect_file_type(filepath)
print(f"Detected: {file_type}")
# Check if compressible
if not should_compress(filepath):
print("Skipping: file is not natural language (code/config)")
sys.exit(0)
print("Starting caveman compression...\n")
try:
success = compress_file(filepath)
if success:
print("\nCompression completed successfully")
backup_path = backup_dir_for(filepath) / (filepath.stem + ".original.md")
print(f"Compressed: {filepath}")
print(f"Original: {backup_path}")
sys.exit(0)
else:
print("\n❌ Compression failed after retries")
sys.exit(2)
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(130)
except Exception as e:
print(f"\n❌ Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
Caveman Memory Compression Orchestrator
Usage:
python scripts/compress.py <filepath>
"""
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import List
OUTER_FENCE_REGEX = re.compile(
r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL
)
# YAML frontmatter: starts at file start with --- on its own line, ends with --- on its own line.
# Captures the entire block (including delimiters and trailing newline) and the body after.
FRONTMATTER_REGEX = re.compile(
r"\A(---\r?\n.*?\r?\n---\r?\n)(.*)", re.DOTALL
)
def split_frontmatter(text: str):
"""Split YAML frontmatter from body. Returns (frontmatter, body).
Memory files (and many other markdown docs) start with a YAML frontmatter
block delimited by `---` lines. The compression LLM has a habit of stripping
or rewriting these despite preserve-structure rules in the prompt — so we
surgically remove the frontmatter before compression and prepend it back
verbatim to the output. Files without frontmatter pass through unchanged.
"""
m = FRONTMATTER_REGEX.match(text)
if m:
return m.group(1), m.group(2)
return "", text
# Filenames and paths that almost certainly hold secrets or PII. Compressing
# them ships raw bytes to the Anthropic API — a third-party data boundary that
# developers on sensitive codebases cannot cross. detect.py already skips .env
# by extension, but credentials.md / secrets.txt / ~/.aws/credentials would
# slip through the natural-language filter. This is a hard refuse before read.
SENSITIVE_BASENAME_REGEX = re.compile(
r"(?ix)^("
r"\.env(\..+)?"
r"|\.netrc"
r"|credentials(\..+)?"
r"|secrets?(\..+)?"
r"|passwords?(\..+)?"
r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?"
r"|authorized_keys"
r"|known_hosts"
r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)"
r")$"
)
SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"})
SENSITIVE_NAME_TOKENS = (
"secret", "credential", "password", "passwd",
"apikey", "accesskey", "token", "privatekey",
)
def backup_dir_for(filepath: Path) -> Path:
"""Resolve the out-of-tree backup directory for a given source file.
Backups must live OUTSIDE the source directory so skill auto-loaders
(Claude Code rules/, opencode instructions/, etc.) stop re-ingesting the
`.original.md` copies as live files. Base dir is platform-aware:
- Windows: %LOCALAPPDATA%\\caveman-compress\\backups
- else: $XDG_DATA_HOME/caveman-compress/backups if set,
else ~/.local/share/caveman-compress/backups
The source file's parent-dir name is mirrored under the base to reduce
cross-project collisions (e.g. two `task.md` files in different repos).
"""
if os.name == "nt" or sys.platform == "win32":
local_appdata = os.environ.get("LOCALAPPDATA")
base = Path(local_appdata) if local_appdata else Path.home() / "AppData" / "Local"
base = base / "caveman-compress" / "backups"
else:
xdg = os.environ.get("XDG_DATA_HOME")
base = Path(xdg) if xdg else Path.home() / ".local" / "share"
base = base / "caveman-compress" / "backups"
return base / filepath.parent.name
def is_sensitive_path(filepath: Path) -> bool:
"""Heuristic denylist for files that must never be shipped to a third-party API."""
name = filepath.name
if SENSITIVE_BASENAME_REGEX.match(name):
return True
lowered_parts = {p.lower() for p in filepath.parts}
if lowered_parts & SENSITIVE_PATH_COMPONENTS:
return True
# Normalize separators so "api-key" and "api_key" both match "apikey".
lower = re.sub(r"[_\-\s.]", "", name.lower())
return any(tok in lower for tok in SENSITIVE_NAME_TOKENS)
def strip_llm_wrapper(text: str) -> str:
"""Strip outer ```markdown ... ``` fence when it wraps the entire output."""
m = OUTER_FENCE_REGEX.match(text)
if m:
return m.group(2)
return text
from .detect import should_compress
from .validate import validate
MAX_RETRIES = 2
# ---------- Claude Calls ----------
def call_claude(prompt: str) -> str:
"""Send a prompt to Claude.
Prefers the Anthropic SDK when ANTHROPIC_API_KEY is set; otherwise falls
back to the ``claude --print`` CLI (which handles desktop auth).
On Windows the CLI subprocess decoding defaults to the system codepage
(cp1251 / cp1252) and crashes on UTF-8 output — see issue #152. Pinning
``encoding="utf-8"`` with ``errors="replace"`` matches the CLI's actual
native I/O and prevents the UnicodeDecodeError before validation can
report. Windows users with non-ASCII content can also set
``ANTHROPIC_API_KEY`` to route through the SDK and skip the subprocess.
"""
api_key = os.environ.get("ANTHROPIC_API_KEY")
if api_key:
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
msg = client.messages.create(
model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"),
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
return strip_llm_wrapper(msg.content[0].text.strip())
except ImportError:
pass # anthropic not installed, fall back to CLI
# Fallback: use claude CLI (handles desktop auth).
# Resolve binary via shutil.which so Windows .cmd/.bat shims (e.g.
# %APPDATA%\npm\claude.CMD) work without shell=True. On POSIX,
# shutil.which returns the same absolute path as the implicit lookup,
# so this is a no-op there. Falls back to bare "claude" if not found
# on PATH so subprocess raises a clear FileNotFoundError.
claude_bin = shutil.which("claude") or "claude"
try:
result = subprocess.run(
[claude_bin, "--print"],
input=prompt,
text=True,
capture_output=True,
check=True,
encoding="utf-8",
errors="replace",
)
return strip_llm_wrapper(result.stdout.strip())
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Claude call failed:\n{e.stderr}")
def build_compress_prompt(original: str) -> str:
return f"""
Compress this markdown into caveman format.
STRICT RULES:
- Do NOT modify anything inside ``` code blocks
- Do NOT modify anything inside inline backticks
- Preserve ALL URLs exactly
- Preserve ALL headings exactly
- Preserve file paths and commands
- Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file.
Only compress natural language.
TEXT:
{original}
"""
def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str:
errors_str = "\n".join(f"- {e}" for e in errors)
return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found.
CRITICAL RULES:
- DO NOT recompress or rephrase the file
- ONLY fix the listed errors — leave everything else exactly as-is
- The ORIGINAL is provided as reference only (to restore missing content)
- Preserve caveman style in all untouched sections
ERRORS TO FIX:
{errors_str}
HOW TO FIX:
- Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED
- Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED
- Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED
- Do not touch any section not mentioned in the errors
ORIGINAL (reference only):
{original}
COMPRESSED (fix this):
{compressed}
Return ONLY the fixed compressed file. No explanation.
"""
# ---------- Core Logic ----------
def compress_file(filepath: Path) -> bool:
# Resolve and validate path
filepath = filepath.resolve()
MAX_FILE_SIZE = 500_000 # 500KB
if not filepath.exists():
raise FileNotFoundError(f"File not found: {filepath}")
if filepath.stat().st_size > MAX_FILE_SIZE:
raise ValueError(f"File too large to compress safely (max 500KB): {filepath}")
# Refuse files that look like they contain secrets or PII. Compressing ships
# the raw bytes to the Anthropic API — a third-party boundary — so we fail
# loudly rather than silently exfiltrate credentials or keys. Override is
# intentional: the user must rename the file if the heuristic is wrong.
if is_sensitive_path(filepath):
raise ValueError(
f"Refusing to compress {filepath}: filename looks sensitive "
"(credentials, keys, secrets, or known private paths). "
"Compression sends file contents to the Anthropic API. "
"Rename the file if this is a false positive."
)
print(f"Processing: {filepath}")
if not should_compress(filepath):
print("Skipping (not natural language)")
return False
original_text = filepath.read_text(errors="ignore")
# Store backup outside the source directory so skill auto-loaders don't
# re-ingest the `.original.md` copy as a live file. Mirror the source's
# parent-dir name + stem under a platform-aware base to reduce collisions.
backup_dir = backup_dir_for(filepath)
backup_dir.mkdir(parents=True, exist_ok=True)
backup_path = backup_dir / (filepath.stem + ".original.md")
if not original_text.strip():
print("❌ Refusing to compress: file is empty or whitespace-only.")
return False
# Check if backup already exists to prevent accidental overwriting
if backup_path.exists():
print(f"⚠️ Backup file already exists: {backup_path}")
print("The original backup may contain important content.")
print("Aborting to prevent data loss. Please remove or rename the backup file if you want to proceed.")
return False
# Split YAML frontmatter off before compression. Claude tends to strip or
# rewrite frontmatter despite preserve-structure rules; we keep it verbatim
# by removing it from the input and re-prepending it to the output.
frontmatter, body = split_frontmatter(original_text)
if frontmatter:
print(f"Detected YAML frontmatter ({len(frontmatter)} chars) — preserving verbatim")
if not body.strip():
print("❌ Refusing to compress: body is empty after frontmatter removal.")
return False
# Step 1: Compress (body only, frontmatter excluded)
print("Compressing with Claude...")
compressed_body = call_claude(build_compress_prompt(body))
if compressed_body is None or not compressed_body.strip():
print("❌ Compression aborted: Claude returned an empty response.")
print(" Original file is untouched (no backup created).")
return False
# Compare the BODY (not the whole file) — frontmatter is preserved verbatim
# and would never change, so identity must be judged on the compressible part.
if compressed_body.strip() == body.strip():
print("❌ Compression aborted: output is identical to input.")
print(" Likely causes: Claude refused, returned the prompt verbatim, or the file is")
print(" already in caveman form. Original file is untouched (no backup created).")
return False
# Reassemble: frontmatter (verbatim) + compressed body
compressed = frontmatter + compressed_body
# Save original as backup, then verify the backup readback before
# touching the input file. If the filesystem dropped bytes (encoding,
# antivirus, disk full), unlink the bad backup and abort instead of
# leaving the user with a corrupt backup + compressed primary.
backup_path.write_text(original_text)
backup_readback = backup_path.read_text(errors="ignore")
if backup_readback != original_text:
print(f"❌ Backup write verification failed: {backup_path}")
print(" In-memory original differs from on-disk backup. Aborting before touching the input file.")
try:
backup_path.unlink()
except OSError:
pass
return False
filepath.write_text(compressed)
# Step 2: Validate + Retry
for attempt in range(MAX_RETRIES):
print(f"\nValidation attempt {attempt + 1}")
result = validate(backup_path, filepath)
if result.is_valid:
print("Validation passed")
break
print("❌ Validation failed:")
for err in result.errors:
print(f" - {err}")
if attempt == MAX_RETRIES - 1:
# Restore original on failure
filepath.write_text(original_text)
backup_path.unlink(missing_ok=True)
print("❌ Failed after retries — original restored")
return False
print("Fixing with Claude...")
compressed = call_claude(
build_fix_prompt(original_text, compressed, result.errors)
)
filepath.write_text(compressed)
return True
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Detect whether a file is natural language (compressible) or code/config (skip)."""
import json
import re
from pathlib import Path
# Extensions that are natural language and compressible
COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst", ".typ", ".typst", ".tex"}
# Extensions that are code/config and should be skipped
SKIP_EXTENSIONS = {
".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml",
".toml", ".env", ".lock", ".css", ".scss", ".html", ".xml",
".sql", ".sh", ".bash", ".zsh", ".go", ".rs", ".java", ".c",
".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".lua",
".dockerfile", ".makefile", ".csv", ".ini", ".cfg",
}
# Well-known build/config files that carry no (or a misleading) extension —
# `Dockerfile` has no suffix so `.dockerfile` above never matches it, and
# `CMakeLists.txt` would ride the compressible `.txt` rule. Checked by
# basename before any extension rule.
KNOWN_CODE_FILENAMES = {
"dockerfile", "makefile", "gnumakefile", "jenkinsfile", "vagrantfile",
"rakefile", "gemfile", "justfile", "procfile", "brewfile",
"cmakelists.txt",
}
# Patterns that indicate a line is code
CODE_PATTERNS = [
re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"),
re.compile(r"^\s*(def |class |function |async function |export )"),
re.compile(r"^\s*(if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{)"),
re.compile(r"^\s*[\}\]\);]+\s*$"), # closing braces/brackets
re.compile(r"^\s*@\w+"), # decorators/annotations
re.compile(r'^\s*"[^"]+"\s*:\s*'), # JSON-like key-value
re.compile(r"^\s*\w+\s*=\s*[{\[\(\"']"), # assignment with literal
]
def _is_code_line(line: str) -> bool:
"""Check if a line looks like code."""
return any(p.match(line) for p in CODE_PATTERNS)
def _is_json_content(text: str) -> bool:
"""Check if content is valid JSON."""
try:
json.loads(text)
return True
except (json.JSONDecodeError, ValueError):
return False
def _is_yaml_content(lines: list[str]) -> bool:
"""Heuristic: check if content looks like YAML."""
yaml_indicators = 0
for line in lines[:30]:
stripped = line.strip()
if stripped.startswith("---"):
yaml_indicators += 1
elif re.match(r"^\w[\w\s]*:\s", stripped):
yaml_indicators += 1
elif stripped.startswith("- ") and ":" in stripped:
yaml_indicators += 1
# If most non-empty lines look like YAML
non_empty = sum(1 for l in lines[:30] if l.strip())
return non_empty > 0 and yaml_indicators / non_empty > 0.6
def detect_file_type(filepath: Path) -> str:
"""Classify a file as 'natural_language', 'code', 'config', or 'unknown'.
Returns:
One of: 'natural_language', 'code', 'config', 'unknown'
"""
ext = filepath.suffix.lower()
# Known code filenames win over any extension rule
if filepath.name.lower() in KNOWN_CODE_FILENAMES:
return "code"
# Extension-based classification
if ext in COMPRESSIBLE_EXTENSIONS:
return "natural_language"
if ext in SKIP_EXTENSIONS:
return "code" if ext not in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env"} else "config"
# Extensionless files (like CLAUDE.md, TODO) — check content
if not ext:
try:
text = filepath.read_text(errors="ignore")
except (OSError, PermissionError):
return "unknown"
lines = text.splitlines()[:50]
# Shebang means executable script, never prose
if text.startswith("#!"):
return "code"
if _is_json_content(text[:10000]):
return "config"
if _is_yaml_content(lines):
return "config"
code_lines = sum(1 for l in lines if l.strip() and _is_code_line(l))
non_empty = sum(1 for l in lines if l.strip())
if non_empty > 0 and code_lines / non_empty > 0.4:
return "code"
return "natural_language"
return "unknown"
def should_compress(filepath: Path) -> bool:
"""Return True if the file is natural language and should be compressed."""
if not filepath.is_file():
return False
# Skip backup files
if filepath.name.endswith(".original.md"):
return False
return detect_file_type(filepath) == "natural_language"
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python detect.py <file1> [file2] ...")
sys.exit(1)
for path_str in sys.argv[1:]:
p = Path(path_str).resolve()
file_type = detect_file_type(p)
compress = should_compress(p)
print(f" {p.name:30s} type={file_type:20s} compress={compress}")
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
import re
from collections import Counter
from pathlib import Path
URL_REGEX = re.compile(r"https?://[^\s)]+")
FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$")
HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
# crude but effective path detection
# Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match
PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+")
class ValidationResult:
def __init__(self):
self.is_valid = True
self.errors = []
self.warnings = []
def add_error(self, msg):
self.is_valid = False
self.errors.append(msg)
def add_warning(self, msg):
self.warnings.append(msg)
def read_file(path: Path) -> str:
return path.read_text(errors="ignore")
# ---------- Extractors ----------
def extract_headings(text):
return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)]
def extract_code_blocks(text):
"""Line-based fenced code block extractor.
Handles ``` and ~~~ fences with variable length (CommonMark: closing
fence must use same char and be at least as long as opening). Supports
nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick
content).
"""
blocks = []
lines = text.split("\n")
i = 0
n = len(lines)
while i < n:
m = FENCE_OPEN_REGEX.match(lines[i])
if not m:
i += 1
continue
fence_char = m.group(2)[0]
fence_len = len(m.group(2))
open_line = lines[i]
block_lines = [open_line]
i += 1
closed = False
while i < n:
close_m = FENCE_OPEN_REGEX.match(lines[i])
if (
close_m
and close_m.group(2)[0] == fence_char
and len(close_m.group(2)) >= fence_len
and close_m.group(3).strip() == ""
):
block_lines.append(lines[i])
closed = True
i += 1
break
block_lines.append(lines[i])
i += 1
if closed:
blocks.append("\n".join(block_lines))
# Unclosed fences are silently skipped — they indicate malformed markdown
# and including them would cause false-positive validation failures.
return blocks
def extract_urls(text):
return set(URL_REGEX.findall(text))
def extract_paths(text):
return set(PATH_REGEX.findall(text))
def count_bullets(text):
return len(BULLET_REGEX.findall(text))
def extract_inline_codes(text):
text_without_fences = re.sub(r"^```[\s\S]*?^```", "", text, flags=re.MULTILINE)
text_without_fences = re.sub(r"^~~~[\s\S]*?^~~~", "", text_without_fences, flags=re.MULTILINE)
return re.findall(r"`([^`]+)`", text_without_fences)
# ---------- Validators ----------
def validate_headings(orig, comp, result):
h1 = extract_headings(orig)
h2 = extract_headings(comp)
if len(h1) != len(h2):
result.add_error(f"Heading count mismatch: {len(h1)} vs {len(h2)}")
if h1 != h2:
result.add_warning("Heading text/order changed")
def validate_code_blocks(orig, comp, result):
c1 = extract_code_blocks(orig)
c2 = extract_code_blocks(comp)
if c1 != c2:
result.add_error("Code blocks not preserved exactly")
def validate_urls(orig, comp, result):
u1 = extract_urls(orig)
u2 = extract_urls(comp)
if u1 != u2:
result.add_error(f"URL mismatch: lost={u1 - u2}, added={u2 - u1}")
def validate_paths(orig, comp, result):
p1 = extract_paths(orig)
p2 = extract_paths(comp)
if p1 != p2:
result.add_warning(f"Path mismatch: lost={p1 - p2}, added={p2 - p1}")
def validate_bullets(orig, comp, result):
b1 = count_bullets(orig)
b2 = count_bullets(comp)
if b1 == 0:
return
diff = abs(b1 - b2) / b1
if diff > 0.15:
result.add_warning(f"Bullet count changed too much: {b1} -> {b2}")
def validate_inline_codes(orig, comp, result):
c1 = Counter(extract_inline_codes(orig))
c2 = Counter(extract_inline_codes(comp))
if c1 != c2:
lost = set(c1.keys()) - set(c2.keys())
added = set(c2.keys()) - set(c1.keys())
for code, count in c1.items():
if code in c2 and c2[code] < count:
lost.add(f"{code} (lost {count - c2[code]} of {count} occurrences)")
if lost:
result.add_error(f"Inline code lost: {lost}")
if added:
result.add_warning(f"Inline code added: {added}")
# ---------- Main ----------
def validate(original_path: Path, compressed_path: Path) -> ValidationResult:
result = ValidationResult()
orig = read_file(original_path)
comp = read_file(compressed_path)
validate_headings(orig, comp, result)
validate_code_blocks(orig, comp, result)
validate_urls(orig, comp, result)
validate_paths(orig, comp, result)
validate_bullets(orig, comp, result)
validate_inline_codes(orig, comp, result)
return result
# ---------- CLI ----------
if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
print("Usage: python validate.py <original> <compressed>")
sys.exit(1)
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
res = validate(orig, comp)
print(f"\nValid: {res.is_valid}")
if res.errors:
print("\nErrors:")
for e in res.errors:
print(f" - {e}")
if res.warnings:
print("\nWarnings:")
for w in res.warnings:
print(f" - {w}")
+38
View File
@@ -0,0 +1,38 @@
# caveman-help
Quick-reference card. One shot, no mode change.
## What it does
Prints a cheat sheet of all caveman modes, sibling skills, deactivation triggers, and how to set the default mode via env var or config file. One-shot display — does not flip the active mode, write flag files, or persist anything. Use when you forget the slash commands.
## How to invoke
```
/caveman-help
```
Also triggers on "caveman help", "what caveman commands", "how do I use caveman".
## Example output
```
Modes:
/caveman full (default)
/caveman lite lighter
/caveman ultra extreme
/caveman wenyan classical Chinese
Skills:
/caveman-commit terse Conventional Commits
/caveman-review one-line PR comments
/caveman-stats session token savings
Deactivate:
"stop caveman" or "normal mode"
```
## See also
- [`SKILL.md`](./SKILL.md) — full reference card
- [Caveman README](../../README.md) — repo overview
+63
View File
@@ -0,0 +1,63 @@
---
name: caveman-help
description: >
Quick-reference card for all caveman modes, skills, and commands.
One-shot display, not a persistent mode. Trigger: /caveman-help,
"caveman help", "what caveman commands", "how do I use caveman".
---
# Caveman Help
Display this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style.
## Modes
| Mode | Trigger | What change |
|------|---------|-------------|
| **Lite** | `/caveman lite` | Drop filler. Keep sentence structure. |
| **Full** | `/caveman` | Drop articles, filler, pleasantries, hedging. Fragments OK. Default. |
| **Ultra** | `/caveman ultra` | Extreme compression. Bare fragments. Tables over prose. |
| **Wenyan-Lite** | `/caveman wenyan-lite` | Classical Chinese style, light compression. |
| **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness. |
| **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget. |
Mode stick until changed or session end.
## Skills
| Skill | Trigger | What it do |
|-------|---------|-----------|
| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |
| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |
| **caveman-compress** | `/caveman-compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. |
| **caveman-help** | `/caveman-help` | This card. |
## Deactivate
Say "stop caveman" or "normal mode". Resume anytime with `/caveman`.
## Language
Keep user's language by default. User write Portuguese → reply Portuguese caveman. Compress the style, not the language. Technical terms, code, commands, commit types, and exact error strings stay verbatim unless user ask for translation.
## Configure Default Mode
Default mode = `full`. Change it:
**Environment variable** (highest priority):
```bash
export CAVEMAN_DEFAULT_MODE=ultra
```
**Config file** (`~/.config/caveman/config.json`):
```json
{ "defaultMode": "lite" }
```
Set `"off"` to disable auto-activation on session start. User can still activate manually with `/caveman`.
Resolution: env var > config file > `full`.
## More
Full docs: https://github.com/JuliusBrussee/caveman
+33
View File
@@ -0,0 +1,33 @@
# caveman-review
One-line PR comments. Location, problem, fix. No throat-clearing.
## What it does
Generates code review comments in `L<line>: <severity> <problem>. <fix>.` format. One line per finding. Severity emoji: 🔴 bug, 🟡 risk, 🔵 nit, ❓ question. Drops "I noticed that...", hedging, and restating what the diff already shows. Keeps exact line numbers, backticked symbols, and concrete fixes.
Auto-clarity: drops terse mode for CVE-class security findings, architectural disagreements, and onboarding contexts where the author needs the *why*. Resumes terse for the rest.
Output only — does not approve, request changes, or run linters.
## How to invoke
```
/caveman-review
```
Also triggers on "review this PR", "code review", "review the diff".
## Example output
```
L42: 🔴 bug: user can be null after .find(). Add guard before .email.
L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.
L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).
L107: ❓ q: why drop the cache here? Reads on next request will miss.
```
## See also
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
- [Caveman README](../../README.md) — repo overview
+55
View File
@@ -0,0 +1,55 @@
---
name: caveman-review
description: >
Ultra-compressed code review comments. Cuts noise from PR feedback while preserving
the actionable signal. Each comment is one line: location, problem, fix. Use when user
says "review this PR", "code review", "review the diff", "/review", or invokes
/caveman-review. Auto-triggers when reviewing pull requests.
---
Write code review comments terse and actionable. One line per finding. Location, problem, fix. No throat-clearing.
## Rules
**Format:** `L<line>: <problem>. <fix>.` — or `<file>:L<line>: ...` when reviewing multi-file diffs.
**Severity prefix (optional, when mixed):**
- `🔴 bug:` — broken behavior, will cause incident
- `🟡 risk:` — works but fragile (race, missing null check, swallowed error)
- `🔵 nit:` — style, naming, micro-optim. Author can ignore
- `❓ q:` — genuine question, not a suggestion
**Drop:**
- "I noticed that...", "It seems like...", "You might want to consider..."
- "This is just a suggestion but..." — use `nit:` instead
- "Great work!", "Looks good overall but..." — say it once at the top, not per comment
- Restating what the line does — the reviewer can read the diff
- Hedging ("perhaps", "maybe", "I think") — if unsure use `q:`
**Keep:**
- Exact line numbers
- Exact symbol/function/variable names in backticks
- Concrete fix, not "consider refactoring this"
- The *why* if the fix isn't obvious from the problem statement
## Examples
❌ "I noticed that on line 42 you're not checking if the user object is null before accessing the email property. This could potentially cause a crash if the user is not found in the database. You might want to add a null check here."
`L42: 🔴 bug: user can be null after .find(). Add guard before .email.`
❌ "It looks like this function is doing a lot of things and might benefit from being broken up into smaller functions for readability."
`L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.`
❌ "Have you considered what happens if the API returns a 429? I think we should probably handle that case."
`L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).`
## Auto-Clarity
Drop terse mode for: security findings (CVE-class bugs need full explanation + reference), architectural disagreements (need rationale, not just a one-liner), and onboarding contexts where the author is new and needs the "why". In those cases write a normal paragraph, then resume terse for the rest.
## Boundaries
Reviews only — does not write the code fix, does not approve/request-changes, does not run linters. Output the comment(s) ready to paste into the PR. "stop caveman-review" or "normal mode": revert to verbose review style.
+30
View File
@@ -0,0 +1,30 @@
# caveman-stats
Real session token receipts. No AI estimation.
## What it does
Reads the current Claude Code session log directly and reports actual input/output token usage plus estimated savings versus a non-caveman baseline. Numbers come from the JSONL session log on disk — the model itself does not compute or estimate them. Output is injected by the `caveman-mode-tracker` hook, which intercepts `/caveman-stats` and returns the formatted stats as a blocked-decision reason.
Each run also writes a lifetime-savings suffix file used by the statusline badge (`⛏ 12.4k`).
## How to invoke
```
/caveman-stats
```
## Example output
```
Session: 47 turns
Input: 12,304 tokens
Output: 3,891 tokens (caveman)
Baseline: 11,247 tokens (estimated without caveman)
Saved: 7,356 tokens (~65%)
```
## See also
- [`SKILL.md`](./SKILL.md) — hook contract and mechanics
- [Caveman README](../../README.md) — repo overview
+10
View File
@@ -0,0 +1,10 @@
---
name: caveman-stats
description: >
Show real token usage and estimated savings for the current session.
Reads directly from the Claude Code session log — no AI estimation.
Triggers on /caveman-stats. Output is injected by the mode-tracker hook;
the model itself does not compute the numbers.
---
This skill is delivered by `hooks/caveman-stats.js` (read by `hooks/caveman-mode-tracker.js` on `/caveman-stats`). The model does not need to do anything when this skill fires — the hook returns `decision: "block"` with the formatted stats as the reason. The user sees the numbers immediately.
+48
View File
@@ -0,0 +1,48 @@
# caveman
Talk like smart caveman. Same brain, fewer tokens.
## What it does
Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts 65% of output tokens (measured) with full accuracy preserved. Mode persists for the whole session until changed or stopped.
Six intensity levels:
| Level | What change |
|-------|-------------|
| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. |
| `full` | Default. Drop articles, fragments OK, short synonyms. |
| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. |
| `wenyan-lite` | Classical Chinese register, light compression. |
| `wenyan-full` | Maximum 文言文. 80-90% character reduction. |
| `wenyan-ultra` | Extreme classical compression. |
Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part.
## How to invoke
```
/caveman # full mode (default)
/caveman lite # lighter compression
/caveman ultra # extreme compression
/caveman wenyan # classical Chinese
stop caveman # back to normal prose
```
## Example output
Question: "Why does my React component re-render?"
Normal prose:
> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue.
Caveman (full):
> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`.
Caveman (ultra):
> Inline obj prop → new ref → re-render. `useMemo`.
## See also
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
- [Caveman README](../../README.md) — repo overview, install, benchmarks
+78
View File
@@ -0,0 +1,78 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts output tokens 65% (measured) by speaking like caveman
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
wenyan-lite, wenyan-full, wenyan-ultra.
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.
## Persistence
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
Default: **full**. Switch: `/caveman lite|full|ultra`.
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No tool-call narration, no decorative tables/emoji, no dumping long raw error logs unless asked — quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations (cfg/impl/req/res/fn) — tokenizer split them same as full word: zero token saved, reader still decode. Full word cheaper AND clearer. No causal arrows (→) either — own token, save nothing. Technical terms exact. Code blocks unchanged. Errors quoted exact.
Preserve user's dominant language. User write Portuguese → reply Portuguese caveman. User write Spanish → reply Spanish caveman. Compress the style, not the language. No forced English openings or status phrases. ALWAYS keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim — unless user explicitly ask for translation.
No self-reference. Never name or announce the style. No "caveman mode on", "me caveman think", no third-person caveman tags. Output caveman-only — never normal answer plus "Caveman:" recap. Exception: user explicitly ask what the mode is.
Pattern: `[thing] [action] [reason]. [next step].`
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
## Intensity
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman. No tool-call narration, no decorative tables/emoji, no long raw error-log dumps unless asked. Standard acronyms OK; no invented abbreviations |
| **ultra** | Strip conjunctions when cause-then-effect stay unambiguous. One word when one word enough. State each fact once. NO prose abbreviations (cfg/impl/req/res/fn/auth), NO arrows (X → Y) — measured zero token saving under tokenizer, cost decode clarity. Code symbols, function names, API names, error strings: never touch |
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
Example — "Why React component re-render?"
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
- ultra: "Inline obj prop, new ref, re-render. `useMemo`."
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "每繪新生對象參照,故重繪;以 useMemo 包之則免。"
- wenyan-ultra: "新參照則重繪。useMemo 包之。"
Example — "Explain database connection pooling."
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
- ultra: "Pool reuse open DB connections. No per-request handshake."
- wenyan-full: "池蓄已開之連,不逐請而新開,省握手之費。"
- wenyan-ultra: "池蓄連,免逐請新開,省握手。"
## Auto-Clarity
Drop caveman when:
- Security warnings
- Irreversible action confirmations
- Multi-step sequences where fragment order or omitted conjunctions risk misread
- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions)
- User asks to clarify or repeats question
Resume caveman after clear part done.
Example — destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
> ```sql
> DROP TABLE users;
> ```
> Caveman resume. Verify backup exist first.
## Boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="szpitale-graph" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="szpitale-graph" options="-parameters" />
</option>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/szpitale-graph/src/main/java" charset="UTF-8" />
</component>
</project>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/szpitale-graph/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="temurin-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+89
View File
@@ -0,0 +1,89 @@
# Szpitale-graph — agent instructions
**TL;DR:** `cd szpitale-graph && ./mvnw -q compile` (wrapper, not `mvn`). Build lives in `szpitale-graph/`. Neo4j via `docker compose up -d` from same dir. CLAUDE.md has full TDD rules and PLAN2.md has the execution backlog. This file covers what CLAUDE.md doesn't.
## Project location & structure
- Application root: `szpitale-graph/` (not the repo root).
- All Maven commands run from `szpitale-graph/` using `./mvnw`.
- Source: `src/main/java/com/developx/szpitale/`
- Tests: `src/test/java/com/developx/szpitale/` (mirror packages)
## Key commands
| Task | Command |
|---|---|
| Compile | `./mvnw -q compile` |
| Test single | `./mvnw -q test -Dtest=ClassName` |
| All tests | `./mvnw -q test` |
| Verify | `./mvnw -q verify` |
| Run CLI | `java -jar target/szpitale-graph.jar` |
| Neo4j up | `docker compose up -d` |
| Neo4j down | `docker compose down -v` |
## Architecture (two layers, JSON between them)
```
ingest (layer 1) ──canonical/*.json──→ load (layer 2 → Neo4j)
```
- **Ingest** = source parsing + normalisation + dedup. No Neo4j dependency.
- **Load** = canonical JSON → UNWIND/MERGE into Neo4j. No network dependency.
- Canonical files: `canonical/<voivodeship>.json` or `canonical/all.json`.
## Code layout (`src/main/java/com/developx/szpitale/`)
| Package | Purpose |
|---|---|
| `model/` | `CanonicalDataset`, `Hospital`, `Person`, `Affiliation`, `Role`, `Mandate`, `RawHospitalRecord` |
| `model/enums/` | `LegalForm`, `RoleType`, `MandateType`, `ConfidenceLevel`, `RoleStatus`, `AffiliationType`, `OrganType`, `SupervisoryBodyType` |
| `ingest/` | `IngestCommand`, `CanonicalWriter`, `IngestResults` |
| `ingest/source/` | `MarkdownRegistrySource` (parses `research-*.md`), `VoivodeshipSource` (interface) |
| `ingest/normalize/` | `NameNormalizer`, `PartyNormalizer`, `Deduplicator` |
| `load/` | `LoadCommand`, `CanonicalReader`, `GraphSchema`, `GraphLoader`, `GraphValidator` |
| `SzpitaleGraphApplication.java` | `@SpringBootApplication` entry point |
## Data format
Research files: `research-<voivodeship>.md` (e.g. `research-podlaskie.md`). Table header:
```
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL) |
```
Every cell **must** have an `https://...` URL. No URL = `BRAK_DANYCH` or skip.
## Current state: build is broken (T1 blocker)
`Deduplicator.java:75``groups` is `Map<String, List<Person>>` but code puts `List<String>`. Fix needed before any test can run. See PLAN2.md §11, task T1.
## Dependencies
- Spring Boot 3.4.4 (spring-shell, neo4j-starter, web)
- Jackson (databind + jsr310)
- Jsoup 1.18.3 (HTML/Mobile Markdown parsing)
- Commons Text 1.12.0 (Jaro-Winkler fuzzy match)
- Commons CSV 1.12.0 (reports)
- Lombok: **declared in pom.xml but has NO `<version>` — add one if you modify it**
- Spring Shell: **declared in pom.xml with empty line — needs `<version>` if you enable it**
- Testcontainers Neo4j 1.20.4 + JUnit Jupiter
## Test conventions
- Tests mirror main package structure.
- Class-name based: `./mvnw -q test -Dtest=NameNormalizerTest`
- Integration tests (`*IT`) use `@Testcontainers` + `Neo4jContainer`.
- Fixture files → `src/test/resources/fixtures/`
## Git / checkpoint protocol
PLAN2.md mandates: one checkpoint commit per task (`-m "T<n>: descript [checkpoint]"`). The `local-agent-progress.json` file tracks task states. On conflicts, **git log is the source of truth** (PLAN2.md §13.5).
## Getting a new voivodeship
1. Deep-research → produce `research-<voiv>.md` in same table format as `research-podlaskie.md`
2. Place file in `old/` or a `research/` directory
3. Run `ingest --voivodeship <voiv>` → generates canonical JSON
4. Run `load --voivodeship <voiv>` → pushes to Neo4j
No code changes needed to add a new region.
+33
View File
@@ -0,0 +1,33 @@
# Szpitale-graph — instrukcje projektu
Aplikacja Java 21 + Maven + Spring Shell budująca graf szpitali w Neo4j. Architektura warstwowa (`ingest` → canonical JSON → `load`). Szczegóły w [PLAN2.md](PLAN2.md).
## Proces tworzenia oprogramowania: TDD (obowiązkowy)
Każda zmiana logiki (nowa klasa, metoda, poprawka buga) powstaje w cyklu **red → green → refactor**. Agent NIE pisze kodu produkcyjnego zanim nie istnieje test, który go wymaga.
### Cykl
1. **RED** — napisz test opisujący pożądane zachowanie. Uruchom go i zobacz, że **failuje** (kompiluje się, ale asercja/logika nie przechodzi). Test bez uprzedniej porażki nic nie dowodzi.
2. **GREEN** — napisz **minimalny** kod produkcyjny, który zazielenia test. Nie dodawaj funkcji, których żaden test nie wymaga.
3. **REFACTOR** — posprzątaj kod i test przy zielonym pasku. Po refaktorze testy nadal zielone.
Powtarzaj małymi krokami. Jeden zachowaniowy przyrost = jeden obieg cyklu.
### Zasady
- **Test najpierw.** Bug fix zaczyna się od testu, który reprodukuje buga (RED), dopiero potem poprawka.
- **Testy jednostkowe** (bez Neo4j) dla: normalizacji (`NameNormalizer`, `PartyNormalizer`), deduplikacji (`Deduplicator`), parsowania źródeł (`MarkdownRegistrySource`), serializacji canonical (`CanonicalWriter`/`CanonicalReader`). Szybkie, deterministyczne, bez I/O sieciowego.
- **Testy integracyjne** dla warstwy `load`: Neo4j przez **Testcontainers** (`org.testcontainers:neo4j`, już w `pom.xml`). Sprawdzają idempotencję `MERGE`, constrainty schematu i raporty walidatora.
- Nazwy testów opisują zachowanie: `metoda_warunek_oczekiwanyWynik` (np. `normalize_polishDiacritics_stripped`).
- Testy w `src/test/java`, mirror pakietów z `src/main/java`.
- Zielony build (`mvn test`) jest warunkiem uznania zadania za zrobione. Jeśli testy nie mogą się uruchomić (brak `mvn`/Neo4j), zgłoś to jawnie — nie deklaruj sukcesu.
### Kolejność przy dług technicznym
Istniejący kod produkcyjny powstał bez testów. Przy dotykaniu klasy bez pokrycia: najpierw dopisz **charakteryzujący** test dla aktualnego zachowania (green), potem prowadź zmianę cyklem TDD.
## Build
- `mvn test` — testy; `mvn compile` — kompilacja; `mvn spring-boot:run` — uruchomienie CLI.
- Neo4j lokalnie: `docker compose up -d` (patrz `szpitale-graph/docker-compose.yml`).
+609
View File
@@ -0,0 +1,609 @@
# PLAN — Graf szpitali (wielowojewódzki), architektura warstwowa Java 21
**Zakres i cel**
- Zbudować graf w Neo4j łączący szpitale w **całej Polsce** (wszystkie 16 województw, nie tylko podlaskie) z ich kadrą kierowniczą, organami nadzorczymi (rada społeczna / rada nadzorcza), partiami/komitetami wyborczymi oraz mandatami samorządowymi (radni sejmiku, radni powiatu, wójtowie/burmistrzowie, posłowie, senatorowie).
- Każdy węzeł osoby i szpitala niesie właściwość `sourceUrl` (lista) wskazującą dokładną stronę źródłową.
- Zachować metadane jakości danych obecne w materiale roboczym (`brak/niepotwierdzona`, `NIEZWERYFIKOWANE`, `brak danych`, rozbieżności źródeł), aby nie utracić kontekstu dziennikarskiego z [research-podlaskie.md](research-podlaskie.md).
**Historia (poprzedni plan, superseded)**
- Pierwsza wersja planu była jednoskryptowa (Python: `requests` + BeautifulSoup, ładowanie przez `neo4j` driver) i ograniczona do województwa podlaskiego. Zastąpiona przez ten plan: **wielowojewódzki**, **warstwowy**, jako aplikacja **Java 21 + Maven + Spring Shell**, gdzie każda warstwa to osobna komenda CLI.
- Wkład starego planu wchłonięty tutaj: tabela źródeł/provenance (sekcja 3→ obecnie 4.6 + 7), zapytania walidacyjne i raport nakładek (→ 5.6), obsługa przypadków brzegowych i rate-limitu (→ 7), lista produktów i checklista wykonawcza (→ 9, 10). Pseudokod Pythona porzucony — logikę realizuje kod Javy warstw `ingest`/`load`.
---
## 1. Architektura warstwowa
Aplikacja to jeden artefakt Spring Boot / Spring Shell z dwiema wyraźnie rozdzielonymi warstwami. Warstwy komunikują się przez **plik(i) w uwspólnionym formacie kanonicznym** (JSON), dzięki czemu można je uruchamiać niezależnie, wznawiać i testować.
```
┌─────────────────────────────────────────────────────────────┐
│ WARSTWA 1: Akwizycja + normalizacja │
│ komenda: ingest │
│ wejście: źródła per-województwo (pliki .md/.csv, konektory) │
│ wyjście: canonical/*.json (Canonical Model, jednolity) │
└─────────────────────────────────────────────────────────────┘
│ canonical JSON
┌─────────────────────────────────────────────────────────────┐
│ WARSTWA 2: Ładowanie do Neo4j + tworzenie powiązań │
│ komenda: load │
│ wejście: canonical/*.json │
│ wyjście: graf w Neo4j (węzły + relacje + provenance) │
└─────────────────────────────────────────────────────────────┘
```
Rozdział warstw plikiem kanonicznym daje:
- **niezależność** — warstwę 1 można uruchomić bez działającego Neo4j; warstwę 2 bez dostępu do sieci;
- **audytowalność** — plik kanoniczny to wersjonowalny, przeglądalny snapshot danych (można trzymać w git);
- **idempotencję** — warstwa 2 opiera się na `MERGE` po kluczach naturalnych, więc wielokrotne ładowanie tego samego pliku nie tworzy duplikatów.
---
## 2. Model kanoniczny (kontrakt między warstwami)
Jeden schemat dla **wszystkich** województw. Plik `canonical/<voivodeship>.json` (np. `podlaskie.json`, `mazowieckie.json`) lub jeden zbiorczy `canonical/all.json`.
```jsonc
{
"schemaVersion": "1.0",
"voivodeship": "podlaskie", // slug województwa (16 możliwych wartości)
"generatedAt": "2026-07-07T10:00:00Z",
"hospitals": [
{
"id": "hosp:podlaskie:usk-bialystok", // stabilny slug (voiv + nazwa)
"name": "Uniwersytecki Szpital Kliniczny w Białymstoku",
"shortName": "USK",
"city": "Białystok",
"voivodeship": "podlaskie",
"legalForm": "SPZOZ", // SPZOZ | SP_PSYCHIATRYCZNY_ZOZ | SPOLKA_Z_OO | ...
"foundingBody": "Uniwersytet Medyczny w Białymstoku",
"supervisoryBodyType": "RADA_SPOLECZNA", // RADA_SPOLECZNA | RADA_NADZORCZA
"nip": null, // gdy dostępny
"krs": null, // dla spółek
"website": "https://uskwb.pl",
"sourceUrls": ["https://uskwb.pl/dyrekcja-szpitala/"]
}
],
"people": [
{
"id": "person:jan-kochanowicz", // slug znormalizowanego imienia+nazwiska
"fullName": "Jan Kochanowicz",
"displayName": "prof. dr hab. n. med. Jan Kochanowicz", // z tytułami, jak w źródle
"titles": ["prof.", "dr hab.", "n. med."],
"sourceUrls": ["https://uskwb.pl/dyrekcja-szpitala/"]
}
],
"affiliations": [ // przynależność partyjna/komitetowa osoby
{
"personId": "person:karol-pilecki",
"type": "PARTIA", // PARTIA | KOMITET_WYBORCZY | KWW_LOKALNY
"name": "Koalicja Obywatelska",
"confidence": "POTWIERDZONA", // POTWIERDZONA | NIEPOTWIERDZONA | NIEZWERYFIKOWANA | BRAK_DANYCH
"note": "jedno źródło prasowe: PO; historycznie PSL",
"sourceUrls": ["https://bip-spzozwszjs.podlaskie.eu/rada.html"]
}
],
"roles": [ // funkcja osoby w szpitalu (krawędź hosp↔person)
{
"hospitalId": "hosp:podlaskie:usk-bialystok",
"personId": "person:jan-kochanowicz",
"roleType": "DYREKTOR", // DYREKTOR | ZASTEPCA_DYREKTORA | GLOWNY_KSIEGOWY
// | CZLONEK_ORGANU_NADZORCZEGO | PRZEWODNICZACY_ORGANU | ...
"roleLabel": "Dyrektor Naczelny", // oryginalna etykieta ze źródła
"organ": "DYREKCJA", // DYREKCJA | RADA_SPOLECZNA | RADA_NADZORCZA
"status": "AKTUALNY", // AKTUALNY | PELNIACY_OBOWIAZKI | ELEKT | BYLY
"validFrom": null, // ISO data, gdy znana
"validTo": null,
"note": "p.o. od 3.03.2025",
"sourceUrls": ["https://uskwb.pl/dyrekcja-szpitala/"]
}
],
"mandates": [ // mandat samorządowy/publiczny osoby (niezależny od szpitala)
{
"personId": "person:marek-olbrys",
"mandateType": "RADNY_SEJMIKU", // RADNY_SEJMIKU | RADNY_POWIATU | RADNY_GMINY
// | WOJT_BURMISTRZ_PREZYDENT | STAROSTA | POSEL | SENATOR | MARSZALEK
"body": "Sejmik Województwa Podlaskiego",
"term": "VII",
"voivodeship": "podlaskie",
"sourceUrls": ["https://www.portalsamorzadowy.pl/osoba/marek-olbrys,876.html"]
}
]
}
```
**Uwagi projektowe:**
- `id` osoby jest generowane deterministycznie z znormalizowanego imienia i nazwiska (patrz normalizacja) — to podstawa deduplikacji **między szpitalami i województwami** (np. osoba zasiadająca w radach kilku szpitali).
- Enumy (`legalForm`, `roleType`, `mandateType`, `confidence`, `status`) są zamknięte i wspólne dla wszystkich województw — to gwarantuje uwspólniony format.
- Pola `note` i `confidence` przenoszą metadane jakościowe z materiału roboczego (kluczowe dla kontekstu dziennikarskiego).
---
## 3. Stack technologiczny
| Element | Wybór |
|---|---|
| Język | Java 21 (records, sealed interfaces, pattern matching, virtual threads) |
| Build | Maven (multi-module opcjonalnie; na start single-module) |
| Framework | Spring Boot 3.x + **Spring Shell 3.x** (komendy CLI) |
| Neo4j | `spring-boot-starter-data-neo4j` **lub** czysty `neo4j-java-driver` (Bolt) |
| HTTP/scraping | `java.net.http.HttpClient` + **jsoup** (parsowanie HTML) |
| JSON | Jackson (`jackson-databind` + `jackson-datatype-jsr310`) |
| Fuzzy-match | `commons-text` (Levenshtein / Jaro-Winkler) do deduplikacji osób |
| CSV (raporty) | `commons-csv` |
| Testy | JUnit 5, Testcontainers (`neo4j` container) do testów warstwy ładowania |
| Konteneryzacja Neo4j | Docker (`neo4j:5`), Bolt na `7687`, UI na `7474` |
### Szkielet projektu Maven
```
szpitale-graph/
├── pom.xml
├── docker-compose.yml # Neo4j 5
├── src/main/java/com/developx/szpitale/
│ ├── SzpitaleGraphApplication.java # @SpringBootApplication, entrypoint Spring Shell
│ ├── model/ # rekordy modelu kanonicznego
│ │ ├── CanonicalDataset.java
│ │ ├── Hospital.java Person.java Affiliation.java Role.java Mandate.java
│ │ └── enums/ (LegalForm, RoleType, MandateType, Confidence, ...)
│ ├── ingest/ # WARSTWA 1
│ │ ├── IngestCommand.java # @ShellComponent — komenda `ingest`
│ │ ├── source/ # źródła per-województwo
│ │ │ ├── VoivodeshipSource.java # interfejs: List<RawRecord> fetch()
│ │ │ ├── MarkdownRegistrySource.java# parser materiałów typu research-*.md
│ │ │ ├── HtmlScraperSource.java # jsoup: strony szpitali/BIP
│ │ │ └── PkwSource.java # weryfikacja afiliacji w PKW 2024
│ │ ├── normalize/
│ │ │ ├── NameNormalizer.java # diakrytyki, tytuły, wielkość liter, slug
│ │ │ ├── PartyNormalizer.java # mapowanie nazw partii/komitetów
│ │ │ └── Deduplicator.java # scalanie osób (fuzzy + reguły)
│ │ └── CanonicalWriter.java # zapis canonical/*.json
│ └── load/ # WARSTWA 2
│ ├── LoadCommand.java # @ShellComponent — komenda `load`
│ ├── CanonicalReader.java # odczyt canonical/*.json
│ ├── GraphSchema.java # constraints + indeksy (Cypher)
│ ├── GraphLoader.java # UNWIND/MERGE węzłów i relacji
│ └── GraphValidator.java # zapytania walidacyjne + raport CSV
└── src/test/java/... # JUnit + Testcontainers
```
---
## 4. WARSTWA 1 — Akwizycja i normalizacja (`ingest`)
### 4.1. Zadanie
Pobrać dane z heterogenicznych źródeł dla wybranego województwa (lub wszystkich) i wyprodukować plik(i) w modelu kanonicznym. **Żadnej zależności od Neo4j.**
### 4.2. Interfejs źródła (pluginowalny per województwo)
```java
public interface VoivodeshipSource {
String voivodeship(); // "podlaskie", "mazowieckie", ...
List<RawHospitalRecord> fetch(); // surowe rekordy przed normalizacją
}
```
Implementacje (wybierane po fladze/konfiguracji):
- **`MarkdownRegistrySource`** — parsuje istniejące materiały robocze w formacie tabel Markdown (jak `research-podlaskie.md`). To ścieżka startowa: podlaskie już mamy. Kolejne województwa dokładamy jako `research-<voiv>.md` (patrz sekcja 4.6 — generowane skillem deep-research).
- **`HtmlScraperSource`** — `HttpClient` + jsoup do stron szpitali (dyrekcja) i BIP (rady). Rate-limiting + retry z backoffem, `User-Agent`, poszanowanie `robots.txt`.
- **`PkwSource`** — kontrola krzyżowa afiliacji radnych w `samorzad2024.pkw.gov.pl` (podnosi `confidence`).
Rejestr źródeł spina wszystkie województwa; brak konfiguracji dla danego województwa = pomijane z ostrzeżeniem w logu (żadnych cichych luk).
### 4.6. Odkrywanie źródeł dla kolejnych województw — skill `deep-research`
Dla podlaskiego materiał źródłowy (`research-podlaskie.md`) już istnieje. Dla pozostałych 15 województw **nie znamy z góry** ani listy szpitali, ani adresów BIP z imiennymi składami rad, ani afiliacji. Ten etap rozpoznawczy realizuje **skill `deep-research`** (fan-out zapytań web, pobranie źródeł, adwersaryjna weryfikacja twierdzeń, synteza raportu z cytowaniami) — jest to krok **poprzedzający** warstwę 1, wykonywany przez agenta, nie przez kod Javy.
**Rola:** deep-research pełni funkcję „discovery" — produkuje dla danego województwa materiał `research-<voiv>.md` w **dokładnie tym samym formacie tabel** co `research-podlaskie.md`, który następnie wchłania `MarkdownRegistrySource` bez żadnych zmian w kodzie. To domyka pętlę: skill znajduje i weryfikuje źródła → zapisuje kanoniczny materiał roboczy → warstwa 1 go normalizuje → warstwa 2 ładuje do grafu.
**Co zlecić skillowi (per województwo), pytania badawcze:**
1. **Lista szpitali** danego województwa z podziałem na: wojewódzkie/uniwersyteckie/resortowe (organ tworzący: samorząd województwa / uczelnia medyczna / MSWiA / MON) oraz powiatowe (organ tworzący: powiat), z formą prawną (SPZOZ vs spółka prawa handlowego).
2. **Kadra kierownicza** każdego szpitala (dyrektor, zastępcy, gł. księgowy) — ze stron „Dyrekcja"/„Kierownictwo" i BIP szpitala.
3. **Skład organu nadzorczego** — rada społeczna (SPZOZ) lub rada nadzorcza (spółka) — z BIP szpitala / uchwał organu tworzącego.
4. **Afiliacje polityczne** członków organów — kontrola krzyżowa w PKW 2024 (`samorzad2024.pkw.gov.pl`), na portalsamorzadowy.pl, w BIP sejmiku/rad powiatów; jawne oznaczenie pewności.
5. **Mandaty samorządowe** (radny sejmiku/powiatu/gminy, wójt/burmistrz/starosta, poseł/senator) osób zasiadających w organach.
**Kontrakt wyjścia skilla** — aby był bezpośrednio parsowalny przez `MarkdownRegistrySource`, deep-research musi wyprodukować:
- nagłówek metodologiczny z datą dostępu i legendą oznaczeń pewności (jak w `research-podlaskie.md`);
- sekcje per szpital z tabelą `| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL) |`;
- **`sourceUrl` przy każdym wierszu** (twardy wymóg — brak URL = wiersz odrzucany/oznaczony `BRAK_DANYCH`);
- markery pewności zgodne z mapowaniem na enum `confidence`: `brak/niepotwierdzona``NIEPOTWIERDZONA`, `NIEZWERYFIKOWANE``NIEZWERYFIKOWANA`, `brak danych``BRAK_DANYCH`;
- sekcję „Rozbieżności, luki i rekomendacje weryfikacyjne" (jak część III materiału podlaskiego).
**Jak wywołać (przykład dla jednego województwa):**
```
/deep-research Zbuduj materiał źródłowy o kadrze kierowniczej, radach społecznych/nadzorczych
i afiliacjach politycznych szpitali w województwie mazowieckim. Zachowaj format
tabel i legendę oznaczeń pewności identyczne jak w research-podlaskie.md. Przy
każdym wpisie podaj dokładny URL źródła (BIP szpitala, uchwały organu tworzącego,
PKW 2024, portalsamorzadowy.pl). Oznacz luki i rozbieżności źródeł.
```
Wynik zapisujemy jako `research-mazowieckie.md`. Skalowanie na wszystkie województwa: uruchamiać skill po jednym województwie (koszt i wolumen źródeł są duże) albo w partiach; każde uruchomienie to osobny plik `research-<voiv>.md`.
**Granica odpowiedzialności:** deep-research **znajduje i weryfikuje źródła** (etap dziennikarsko-badawczy, wymaga oceny człowieka przed publikacją, zwłaszcza dla oznaczeń `NIEZWERYFIKOWANA`). Kod Javy (warstwa 1) **nie prowadzi researchu** — jedynie deterministycznie normalizuje gotowy materiał `.md` do modelu kanonicznego. Ten podział utrzymuje warstwę 1 testowalną i powtarzalną, a odkrywanie źródeł — audytowalne i cytowane.
### 4.3. Normalizacja do formatu uwspólnionego
1. **Nazwiska** (`NameNormalizer`): usunięcie tytułów do `titles[]`, `displayName` zachowuje oryginał, `fullName` = imię+nazwisko, `id` = slug z NFKD bez diakrytyków, lower-case, `-`. Uwaga na warianty (np. `Wojciech Łuczaj` występuje w kilku szpitalach).
2. **Partie/komitety** (`PartyNormalizer`): mapowanie na kanoniczne nazwy (`Koalicja Obywatelska`, `PiS`, `PSL`, `Polska 2050`, `Trzecia Droga`, ...) + typ `PARTIA`/`KOMITET_WYBORCZY`/`KWW_LOKALNY`. `confidence` z markerów źródła (`NIEZWERYFIKOWANE``NIEZWERYFIKOWANA`, `brak danych``BRAK_DANYCH`, `brak/niepotwierdzona``NIEPOTWIERDZONA`).
3. **Funkcje** (`roleType`/`organ`/`status`): mapowanie etykiet („Dyrektor", „p.o. Dyrektora" → status `PELNIACY_OBOWIAZKI`, „Dyrektor-elekt" → `ELEKT`, „Rada Społeczna — Przewodniczący" → `PRZEWODNICZACY_ORGANU` + `organ=RADA_SPOLECZNA`).
4. **Deduplikacja osób** (`Deduplicator`): scalenie po `id`; przy zbieżnych, ale niepewnych dopasowaniach (pospolite nazwiska: Możejko, Jabłoński, Dzięcioł) — **nie scalać**, dodać `note="możliwy duplikat"` i zachować odrębne węzły. Fuzzy-match (Jaro-Winkler) tylko jako podpowiedź do przeglądu, nie jako automatyczne scalenie.
### 4.4. Komenda Spring Shell
```java
@ShellComponent
public class IngestCommand {
@ShellMethod(key = "ingest",
value = "Warstwa 1: pobiera i normalizuje dane szpitali do modelu kanonicznego (JSON).")
public String ingest(
@ShellOption(defaultValue = "all") String voivodeship, // np. podlaskie | mazowieckie | all
@ShellOption(defaultValue = "markdown") String source, // markdown | html | pkw
@ShellOption(defaultValue = "canonical") String outDir, // katalog wyjściowy
@ShellOption(defaultValue = "false") boolean split // plik per województwo vs all.json
) {
// 1. wybierz źródła dla województw
// 2. fetch -> normalize -> dedup
// 3. zapisz canonical/<voiv>.json (lub all.json)
// 4. zwróć podsumowanie: #szpitali, #osób, #ról, #mandatów, #luk
}
}
```
Przykłady użycia:
```
ingest --voivodeship podlaskie --source markdown
ingest --voivodeship all --split true
ingest --voivodeship mazowieckie --source html --out-dir canonical
```
### 4.5. Produkt warstwy 1
- `canonical/<voiv>.json` (lub `canonical/all.json`) zgodny ze schematem z sekcji 2.
- `ingest-report.json` — statystyki i lista luk (szpitale bez składu rady, afiliacje `NIEZWERYFIKOWANA`) — odpowiednik „Głównych luk" z materiału roboczego.
---
## 5. WARSTWA 2 — Ładowanie do Neo4j i tworzenie powiązań (`load`)
### 5.1. Zadanie
Odczytać plik(i) kanoniczne i zmaterializować graf w Neo4j: węzły, relacje i provenance. **Żadnej zależności od sieci/scrapingu.** Idempotentne (`MERGE`).
### 5.2. Model grafu
**Węzły (labels):**
- `:Hospital {id, name, shortName, city, voivodeship, legalForm, foundingBody, supervisoryBodyType, nip, krs, website, sourceUrls}`
- `:Person {id, fullName, displayName, titles, sourceUrls}`
- `:Party {name, type}` (partia/komitet)
- `:GovBody {name, type, voivodeship, term}` (sejmik, rada powiatu, urząd gminy — dla mandatów)
- `:Voivodeship {slug, name}` (grupowanie i zapytania regionalne)
**Relacje:**
| Relacja | Od → Do | Właściwości |
|---|---|---|
| `[:DYREKTOR]` | Hospital → Person | `roleLabel, status, validFrom, validTo, note, sourceUrls` |
| `[:ZASTEPCA_DYREKTORA]` | Hospital → Person | j.w. |
| `[:CZLONEK_ORGANU]` | Hospital → Person | `organ (RADA_SPOLECZNA/RADA_NADZORCZA), roleLabel, status, sourceUrls` |
| `[:PRZEWODNICZY_ORGANOWI]` | Person → Hospital | `organ, sourceUrls` |
| `[:CZLONEK_PARTII]` | Person → Party | `confidence, note, sourceUrls` |
| `[:PELNI_MANDAT]` | Person → GovBody | `mandateType, term, sourceUrls` |
| `[:W_WOJEWODZTWIE]` | Hospital → Voivodeship | — |
> Uwaga: zamiast osobnych typów relacji dla każdej funkcji można użyć jednej `[:PELNI_FUNKCJE {roleType, organ, ...}]`. Wybór: **typowane relacje** dla czytelności zapytań Cypher + `roleType` jako property dla filtrowania.
### 5.3. Schemat (constraints + indeksy)
```cypher
CREATE CONSTRAINT hospital_id IF NOT EXISTS FOR (h:Hospital) REQUIRE h.id IS UNIQUE;
CREATE CONSTRAINT person_id IF NOT EXISTS FOR (p:Person) REQUIRE p.id IS UNIQUE;
CREATE CONSTRAINT party_name IF NOT EXISTS FOR (x:Party) REQUIRE x.name IS UNIQUE;
CREATE CONSTRAINT voiv_slug IF NOT EXISTS FOR (v:Voivodeship) REQUIRE v.slug IS UNIQUE;
CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.fullName);
```
### 5.4. Ładowanie (parametryzowane, wsadowe)
```cypher
UNWIND $hospitals AS h
MERGE (hosp:Hospital {id: h.id})
SET hosp += {name:h.name, city:h.city, voivodeship:h.voivodeship,
legalForm:h.legalForm, supervisoryBodyType:h.supervisoryBodyType,
website:h.website, sourceUrls:h.sourceUrls}
MERGE (v:Voivodeship {slug: h.voivodeship})
MERGE (hosp)-[:W_WOJEWODZTWIE]->(v);
UNWIND $people AS p
MERGE (person:Person {id: p.id})
SET person += {fullName:p.fullName, displayName:p.displayName,
titles:p.titles, sourceUrls:p.sourceUrls};
UNWIND $roles AS r
MATCH (h:Hospital {id:r.hospitalId}), (p:Person {id:r.personId})
CALL apoc.merge.relationship(h, r.relType, {}, r.props, p) YIELD rel // lub CASE per typ bez APOC
RETURN count(*);
UNWIND $affiliations AS a
MATCH (p:Person {id:a.personId})
MERGE (party:Party {name:a.name}) SET party.type = a.type
MERGE (p)-[m:CZLONEK_PARTII]->(party)
SET m += {confidence:a.confidence, note:a.note, sourceUrls:a.sourceUrls};
UNWIND $mandates AS md
MATCH (p:Person {id:md.personId})
MERGE (g:GovBody {name:md.body}) SET g.type = md.mandateType, g.voivodeship = md.voivodeship, g.term = md.term
MERGE (p)-[:PELNI_MANDAT {mandateType:md.mandateType, term:md.term, sourceUrls:md.sourceUrls}]->(g);
```
Batche po ~510 tys. wierszy w jednej transakcji (tu wolumen mały, ale trzymamy wzorzec).
> Jeśli nie chcemy zależności od APOC — w `GraphLoader` mapujemy `roleType` na stały typ relacji przez `switch` i wykonujemy osobny sparametryzowany `MERGE` per typ.
### 5.5. Komenda Spring Shell
```java
@ShellComponent
public class LoadCommand {
@ShellMethod(key = "load",
value = "Warstwa 2: ładuje model kanoniczny do Neo4j i tworzy powiązania.")
public String load(
@ShellOption(defaultValue = "canonical") String inDir, // katalog z canonical/*.json
@ShellOption(defaultValue = "all") String voivodeship, // filtr: które pliki załadować
@ShellOption(defaultValue = "true") boolean createSchema,// czy zakładać constraints/indeksy
@ShellOption(defaultValue = "false") boolean wipe, // wyczyścić graf przed ładowaniem
@ShellOption(defaultValue = "true") boolean validate // uruchomić walidację po ładowaniu
) {
// 1. (opc.) wipe + createSchema
// 2. read canonical -> UNWIND/MERGE nodes -> relationships
// 3. (opc.) validate + zapis overlap_report.csv
// 4. podsumowanie: #węzłów, #relacji per typ
}
}
```
Konfiguracja połączenia w `application.yml`:
```yaml
spring:
neo4j:
uri: bolt://localhost:7687
authentication: { username: neo4j, password: password }
```
Przykłady:
```
load --in-dir canonical --voivodeship all --wipe true
load --voivodeship podlaskie --create-schema false
```
### 5.6. Walidacja i raport (`GraphValidator`)
```cypher
// Szpitale bez dyrektora (luki do uzupełnienia)
MATCH (h:Hospital) WHERE NOT (h)-[:DYREKTOR]->() RETURN h.name, h.voivodeship;
// Osoby łączące funkcję w organie szpitala z mandatem samorządowym (kluczowy wniosek analityczny)
MATCH (h:Hospital)-[:CZLONEK_ORGANU]->(p:Person)-[:PELNI_MANDAT]->(g:GovBody)
RETURN p.fullName, collect(DISTINCT h.name) AS szpitale, collect(DISTINCT g.name) AS mandaty;
// Osoby zasiadające w organach wielu szpitali (powiązania międzyszpitalne / międzywojewódzkie)
MATCH (h:Hospital)-[:CZLONEK_ORGANU]->(p:Person)
WITH p, collect(DISTINCT h) AS hs WHERE size(hs) > 1
RETURN p.fullName, [x IN hs | x.name] AS szpitale;
```
Eksport `overlap_report.csv` (osoba, szpitale, funkcje, partia, mandaty, `confidence`, `sourceUrls`).
---
## 6. Rozszerzanie na kolejne województwa
Aby dodać województwo (np. mazowieckie):
1. **Odkrycie źródeł skillem `deep-research`** (sekcja 4.6): wygenerować `research-mazowieckie.md` w formacie tabel identycznym z `research-podlaskie.md` — z URL-ami źródeł, oznaczeniami pewności i sekcją luk/rozbieżności.
2. Dostarczyć źródło danych warstwie 1: materiał `research-mazowieckie.md` (obsłuży `MarkdownRegistrySource`) lub — dla źródeł ustrukturyzowanych — konfigurację scrapera BIP dla `HtmlScraperSource`.
3. Zarejestrować źródło w rejestrze województw (bean/konfiguracja) — **bez zmian w warstwie 2**.
4. Uruchomić `ingest --voivodeship mazowieckie``load --voivodeship mazowieckie`.
Model kanoniczny, schemat grafu i komendy pozostają niezmienne — nowe województwo to tylko nowe dane w tym samym kontrakcie. Slug województwa jest wymiarem we wszystkich zapytaniach, co umożliwia analizy krajowe i porównania międzyregionalne.
---
## 7. Obsługa błędów i przypadków brzegowych
| Sytuacja | Strategia |
|---|---|
| Brak strony ze składem organu | `role`/`affiliation` z `confidence=BRAK_DANYCH`, wpis w `ingest-report.json` (luka) |
| Identyczne nazwiska, różne osoby | Nie scalać; odrębne węzły + `note` „możliwy duplikat"; fuzzy-match tylko jako podpowiedź |
| Rozbieżne źródła afiliacji | Zachować obie w `note`, `confidence=NIEZWERYFIKOWANA`, obie w `sourceUrls` |
| Migracja domen BIP (`wrotapodlasia.pl``podlaskie.eu`) | Retry + fallback URL; log nieodpowiadających adresów |
| Dane dynamiczne (zmiany kadr) | `validFrom`/`validTo` + `status` na relacji funkcji (`AKTUALNY`/`PELNIACY_OBOWIAZKI`/`ELEKT`/`BYLY`) |
| Rate-limit / blokada scrapera | Exponential backoff; przełączenie na materiał `.md` jako cache; adnotacja źródła |
| Spółka z o.o. (rada nadzorcza) vs SPZOZ (rada społeczna) | `supervisoryBodyType` + `organ` na relacji rozróżniają organy |
---
## 8. Uruchomienie (end-to-end)
```bash
# 0. Neo4j
docker compose up -d # neo4j:5, Bolt 7687, UI 7474
# 1. Build
mvn clean package # Java 21, Spring Boot fat-jar
# 2. Uruchom powłokę Spring Shell
java -jar target/szpitale-graph.jar
# 3. W powłoce — warstwa 1, potem warstwa 2
shell:> ingest --voivodeship all --split true
shell:> load --voivodeship all --wipe true --validate true
# alternatywnie tryb nieinteraktywny (jedna komenda na wywołanie):
java -jar target/szpitale-graph.jar ingest --voivodeship podlaskie
java -jar target/szpitale-graph.jar load --voivodeship podlaskie
```
---
## 9. Produkty (deliverables)
1. **Aplikacja Java 21 / Maven / Spring Shell** z komendami `ingest` (warstwa 1) i `load` (warstwa 2).
2. **Model kanoniczny** (`canonical/*.json`) — wersjonowalny, wielowojewódzki snapshot danych.
3. **Schemat Neo4j** — udokumentowane labels, typy relacji i właściwości (sekcja 5.25.3).
4. **`overlap_report.csv`** — osoby łączące funkcje w organach szpitali z mandatami/partiami, z `sourceUrls` i `confidence`.
5. **`ingest-report.json`** — statystyki i rejestr luk/rozbieżności per województwo.
6. **Testy** — JUnit + Testcontainers (Neo4j) dla warstwy ładowania; testy jednostkowe normalizacji/deduplikacji.
---
## 10. Checklista wykonawcza
- [ ] `pom.xml`: Java 21, Spring Boot 3.x, Spring Shell, Neo4j driver, jsoup, Jackson, commons-text/csv.
- [ ] Rekordy modelu kanonicznego + enumy (sekcja 2).
- [ ] Warstwa 1: `VoivodeshipSource` + `MarkdownRegistrySource` (start: parser `research-podlaskie.md`).
- [ ] Normalizacja: `NameNormalizer`, `PartyNormalizer`, `Deduplicator`.
- [ ] Komenda `ingest` + zapis `canonical/*.json` + `ingest-report.json`.
- [ ] Warstwa 2: `GraphSchema`, `GraphLoader` (UNWIND/MERGE), `GraphValidator`.
- [ ] Komenda `load` + `overlap_report.csv`.
- [ ] `docker-compose.yml` (Neo4j 5) + `application.yml` (Bolt, credentials).
- [ ] Testy: parsowanie MD, normalizacja nazwisk, ładowanie do Testcontainers-Neo4j, zapytania walidacyjne.
- [ ] Weryfikacja end-to-end na podlaskim, następnie dołożenie kolejnego województwa: `deep-research``research-<voiv>.md``ingest``load`.
- [ ] Dla każdego nowego województwa: uruchomić skill `deep-research` (sekcja 4.6), zweryfikować oznaczenia `NIEZWERYFIKOWANA` przed użyciem, zapisać `research-<voiv>.md`.
---
## 11. Kolejka zadań dla lokalnego agenta (TDD, kroki atomowe)
Ta sekcja jest pisana pod **mały lokalny model** o ograniczonym kontekście. Każde zadanie jest samowystarczalne: jeden plik lub jedna metoda, dokładna ścieżka, dokładny test, jedna komenda weryfikacji, twarde kryterium ukończenia. Agent **nie planuje** — bierze pierwsze niezrobione zadanie i wykonuje je w całości.
### Reguły pracy (przeczytaj przed każdym zadaniem)
1. **Bierz DOKŁADNIE jedno zadanie** z listy poniżej, od góry. Nie łącz zadań.
2. **Nie dotykaj plików spoza zadania.** Jeśli zadanie mówi „plik X", edytuj tylko X i jego test.
3. **Cykl TDD (obowiązkowy, patrz `CLAUDE.md`):**
- napisz test w `src/test/java/...` (mirror pakietu),
- uruchom `./mvnw -q test -Dtest=NazwaTestu` i zobacz **RED** (czerwony),
- napisz minimalny kod produkcyjny → **GREEN**,
- posprzątaj przy zielonym → **REFACTOR**.
4. **Komenda z katalogu `szpitale-graph/`.** Zawsze `./mvnw` (jest wrapper), nigdy `mvn`.
5. **Definicja ukończenia zadania:** `./mvnw -q test` przechodzi na zielono ORAZ nie ma nowych ostrzeżeń kompilatora dla dotkniętego pliku.
6. **Jeśli utkniesz > 2 próby** — zostaw plik bez zmian, dopisz jednolinijkową notatkę `// TODO(local-agent): <opis blokady>` i przejdź dalej NIE psując builda.
7. **Nie wymyślaj API.** Używaj tylko klas/metod, które już istnieją w `src/main/java` (sprawdź plik zanim wywołasz metodę).
8. Zadania z tagiem **[NEO4J]** wymagają Dockera (Testcontainers). Jeśli `docker` niedostępny — pomiń, zostaw zadanie odhaczone jako `[~]` z notatką „brak Dockera".
9. **Zapisz punkt kontrolny po KAŻDYM zadaniu** (patrz §13). Bez zapisanego checkpointu zadanie NIE jest ukończone, nawet jeśli testy są zielone.
### Legenda statusów: `[ ]` niezrobione · `[x]` zrobione · `[~]` pominięte (z notatką)
---
**T1 — Naprawić kompilację w `Deduplicator` [BLOKER, robić pierwsze]**
- Plik: `src/main/java/com/developx/szpitale/ingest/normalize/Deduplicator.java`, metoda `checkFuzzyDuplicates`.
- Objaw: `groups` jest typu `Map<String, List<Person>>`, a kod wkłada do niego `List<String>` (błąd kompilacji ~linia 75). Zmienna `personIds` jest nieużywana, a pętla `for (String existingId : groups.keySet())` iteruje po pustej mapie — nigdy nie porówna z już-widzianymi osobami.
- Zamiar metody: dla każdej osoby znaleźć **już przetworzone** osoby o podobnym nazwisku (Jaro-Winkler w przedziale `[SUSPECT_THRESHOLD, DUPLICATE_THRESHOLD)`) i wypisać ostrzeżenie o możliwych duplikatach. **Nie scalać** (patrz sekcja 4.3 pkt 4).
- TDD:
1. Test `DeduplicatorTest.checkFuzzyDuplicates_similarNames_reportedNotMerged`: podaj listę 2 osób o bardzo podobnych `fullName` (np. `Jan Kowalski` / `Jan Kowaski`) o różnych `id`; asercja: obie osoby nadal istnieją jako odrębne węzły (brak scalenia). RED najpierw.
2. Minimalny fix: zmień typ na `Map<String, List<String>>` (id → lista podobnych id) i porównuj bieżącą osobę z **wcześniej odwiedzonymi** (np. utrzymuj `List<Person> seen` i iteruj po niej), a nie po `groups.keySet()`. Popraw wypis, by używał `candidates`, nie `personIds`; usuń `personIds`.
- DoD: `./mvnw -q compile` przechodzi; `./mvnw -q test -Dtest=DeduplicatorTest` zielony.
**T2 — Testy `NameNormalizer` (charakteryzujące + slug)**
- Plik prod: `src/main/java/com/developx/szpitale/ingest/normalize/NameNormalizer.java` (istnieje — najpierw go przeczytaj, nazwy metod bierz z pliku).
- Test: `NameNormalizerTest` w `src/test/java/com/developx/szpitale/ingest/normalize/`.
- Przypadki (po jednym asercie na test):
- `normalize_polishDiacritics_strippedToAsciiSlug` → wejście `Łukasz Żółć` daje slug bez diakrytyków, lower-case, separator `-` (np. `lukasz-zolc`).
- `normalize_academicTitles_movedToTitlesList``prof. dr hab. Jan Kochanowicz``fullName == "Jan Kochanowicz"`, `titles` zawiera `prof.`, `dr hab.`.
- `normalize_displayName_keepsOriginal``displayName` zachowuje oryginał z tytułami.
- DoD: `./mvnw -q test -Dtest=NameNormalizerTest` zielony. Jeśli zachowanie kodu odbiega od oczekiwań — to jest RED; napraw kod minimalnie, chyba że wymagałoby to zmiany API (wtedy `// TODO(local-agent)` i pomiń dany przypadek).
**T3 — Testy `PartyNormalizer` (mapowanie nazw + confidence)**
- Plik prod: `.../ingest/normalize/PartyNormalizer.java` (przeczytaj metody).
- Test: `PartyNormalizerTest`.
- Przypadki:
- `mapParty_knownAlias_canonicalName``PO` / `KO``Koalicja Obywatelska` (użyj aliasu, który realnie jest w mapie w kodzie).
- `mapConfidence_marker_toEnum` → marker `NIEZWERYFIKOWANE``ConfidenceLevel.NIEZWERYFIKOWANA`; `brak danych``BRAK_DANYCH`; `brak/niepotwierdzona``NIEPOTWIERDZONA`.
- DoD: `./mvnw -q test -Dtest=PartyNormalizerTest` zielony.
**T4 — Test `MarkdownRegistrySource` na małym fixture**
- Plik prod: `.../ingest/source/MarkdownRegistrySource.java` (przeczytaj sygnaturę `fetch()` / konstruktor).
- Fixture: utwórz `src/test/resources/fixtures/research-mini.md` z JEDNYM szpitalem i tabelą `| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL) |` z 1 wierszem zawierającym URL.
- Test: `MarkdownRegistrySourceTest.fetch_miniFixture_returnsOneHospitalRecord` → asercja: dokładnie 1 `RawHospitalRecord`, z niepustym `sourceUrl`.
- Zasada twarda (sekcja 4.6): **wiersz bez URL** → odrzucony lub oznaczony `BRAK_DANYCH`. Dodaj drugi test `fetch_rowWithoutUrl_flaggedOrSkipped` na to.
- DoD: `./mvnw -q test -Dtest=MarkdownRegistrySourceTest` zielony.
**T5 — Round-trip `CanonicalWriter``CanonicalReader`**
- Pliki prod: `.../ingest/CanonicalWriter.java`, `.../load/CanonicalReader.java`.
- Test: `CanonicalRoundTripTest.write_thenRead_yieldsEqualDataset` → zbuduj mały `CanonicalDataset` (1 szpital, 1 osoba, 1 rola), zapisz do pliku tymczasowego (`@TempDir`), odczytaj i porównaj pola. RED najpierw (może ujawnić brak modułu JSR-310 lub złą serializację enumów).
- DoD: `./mvnw -q test -Dtest=CanonicalRoundTripTest` zielony.
**T6 — [NEO4J] Idempotencja `GraphLoader` (Testcontainers)**
- Pliki prod: `.../load/GraphSchema.java`, `.../load/GraphLoader.java`.
- Test integracyjny: `GraphLoaderIT` z `@Testcontainers` + `Neo4jContainer` (dependency już w `pom.xml`).
- Przypadki:
- `load_twice_isIdempotent` → załaduj ten sam mały dataset 2× i sprawdź, że liczba węzłów `:Hospital`/`:Person` się nie podwaja (skutek `MERGE`).
- `schema_constraints_created` → po `GraphSchema` istnieje constraint unikalności na `Hospital.id`.
- Wymaga Dockera. Bez Dockera → `[~]` + notatka.
- DoD: `./mvnw -q test -Dtest=GraphLoaderIT` zielony (lub pominięte z notatką).
**T7 — [NEO4J] Zapytania walidatora + `overlap_report.csv`**
- Plik prod: `.../load/GraphValidator.java`.
- Test integracyjny `GraphValidatorIT`: wgraj dataset, w którym jedna osoba ma i `CZLONEK_ORGANU` (szpital) i `PELNI_MANDAT` (GovBody); asercja: raport nakładek zawiera tę osobę; plik `overlap_report.csv` powstaje i ma nagłówek z sekcji 5.6.
- DoD: `./mvnw -q test -Dtest=GraphValidatorIT` zielony (lub pominięte z notatką).
**T8 — Zielony pełny build**
- Uruchom `./mvnw -q test` (wszystko). Cel: brak błędów, brak nowych ostrzeżeń.
- Jeśli któreś [NEO4J] pominięte — odnotuj w tej checklistcie jako `[~]` z powodem.
- DoD: `./mvnw -q verify` przechodzi (pomijając wyłącznie zadania bez Dockera).
---
## 12. Sekcja postępu (aktualizuje lokalny agent)
Po ukończeniu zadania agent zmienia jego status w liście poniżej i dopisuje jedną linię do dziennika.
- [ ] T1 Deduplicator compile fix
- [ ] T2 NameNormalizer testy
- [ ] T3 PartyNormalizer testy
- [ ] T4 MarkdownRegistrySource test
- [ ] T5 Canonical round-trip
- [ ] T6 GraphLoader idempotencja [NEO4J]
- [ ] T7 GraphValidator + CSV [NEO4J]
- [ ] T8 Zielony pełny build
**Dziennik (data — zadanie — wynik):**
- (pusto)
---
## 13. Punkty kontrolne i wznawianie (odporność na śmierć agenta)
Cel: jeśli lokalny agent zostanie ubity w połowie (crash, wyczerpany kontekst, restart), **następne uruchomienie musi kontynuować od ostatniego bezpiecznego stanu**, bez powtarzania zrobionej pracy i bez zostawiania połowicznych zmian.
Mechanizm opiera się na dwóch trwałych źródłach prawdy:
- **git** — jeden commit = jeden ukończony punkt kontrolny (kod jest w historii, nie tylko w drzewie roboczym);
- **`local-agent-progress.json`** (w katalogu `szpitale-graph/`) — maszynowy stan zadań, czytany na starcie.
### 13.1. Procedura STARTU (zawsze na początku uruchomienia)
1. `cd szpitale-graph`
2. Przeczytaj `local-agent-progress.json`.
3. `git status --porcelain` — sprawdź drzewo robocze:
- **Czyste** → wznów od pierwszego zadania o statusie `todo`.
- **Brudne** (są niezacommitowane zmiany) → oznacza to przerwanie w połowie zadania `in_progress`. Wykonaj **odzysk**: `git checkout -- . && git clean -fd` (odrzuć połowiczną pracę), ustaw to zadanie z powrotem na `todo` w JSON, i zacznij je od zera. Połowiczny kod jest zawsze do wyrzucenia, bo checkpoint zapisujemy tylko przy zielonych testach.
4. Zweryfikuj punkt startowy: `./mvnw -q test` powinno odzwierciedlać ostatni checkpoint (zielone dla zrobionych zadań; czerwone tylko dla T1 na samym starcie projektu).
### 13.2. Procedura na WEJŚCIU w zadanie
1. Ustaw status zadania w JSON na `in_progress`, wpisz `startedAt` (data z systemu).
2. **Zacommituj samą tę zmianę stanu**: `git add local-agent-progress.json && git commit -m "T<n>: start"`. Dzięki temu drzewo jest czyste w trakcie pracy nad kodem, a przerwanie łatwo wykryć (jakiekolwiek brudne pliki = połowiczna praca do odrzucenia).
### 13.3. Procedura ZAPISU punktu kontrolnego (po zielonych testach)
Wykonuj dopiero gdy `./mvnw -q test` (lub `-Dtest=...` dla danego zadania) jest **zielone**:
1. Zaktualizuj `local-agent-progress.json`: status `done`, `finishedAt`, krótka `note`.
2. Zaktualizuj checklistę w §12 (`[ ]``[x]` / `[~]`) i dopisz linię do dziennika §12.
3. Jeden atomowy commit obejmujący kod + testy + JSON + PLAN:
```
git add -A && git commit -m "T<n>: <krótki opis> [checkpoint]"
```
4. Po commicie drzewo jest czyste → to jest bezpieczny punkt wznowienia.
### 13.4. Niezmienniki (muszą zachodzić między zadaniami)
- **Każdy commit buduje się** — nigdy nie commituj czerwonego builda (wyjątek: jedyny dozwolony czerwony stan to punkt WYJŚCIOWY projektu przed T1).
- **Jeden checkpoint = jedno zadanie** — nie łącz dwóch zadań w jeden commit.
- **Brudne drzewo == praca w toku do odrzucenia** — nigdy nie da się „częściowo ukończonego" zadania odzyskać; zawsze restart zadania od zera. To celowe uproszczenie: agent nie musi rozumować o połowicznym stanie.
- **JSON i git zgadzają się** — jeśli JSON mówi `done` a `git log` nie ma checkpointu tego zadania (lub odwrotnie), źródłem prawdy jest **git**: napraw JSON do stanu wynikającego z historii commitów, potem kontynuuj.
### 13.5. Format `local-agent-progress.json`
Pełny bieżący stan jest w pliku `szpitale-graph/local-agent-progress.json`. Schemat pola `tasks[]`:
- `id` (np. `"T1"`), `title`, `status` (`todo` | `in_progress` | `done` | `skipped`),
- `startedAt`, `finishedAt` (ISO-8601 lub `null`), `note` (string).
Wznawianie sprowadza się do: wczytaj JSON → znajdź pierwszy `todo`/`in_progress` → (jeśli `in_progress` + brudne drzewo) odzysk wg §13.1 → wykonuj wg §13.213.3.
---
**Koniec planu**
+84
View File
@@ -0,0 +1,84 @@
# Hospital Leadership Graph Database
This repository contains the implementation for building a Neo4j graph database of Polish hospitals and their leadership structures.
## Overview
The project creates a graph database that links:
- **Hospitals** in Poland → **Directors** (`[:DYREKTOR]`)
- **Hospitals****Supervisory Boards** (`[:RADNIA]`)
- **Supervisory Board members****Political parties** (`[:CZŁONEK_PARTII]`)
- **Supervisory Board members****Regional councilors** (`[:RADNY]`) when the same person holds both roles
## Implementation
The implementation consists of:
1. **build_hospital_graph.py** - Main Python script that:
- Sets up Neo4j connection and schema
- Processes hospital data (using sample data as real API is not accessible)
- Processes councilor data (using sample data)
- Creates graph nodes and relationships
- Generates summary reports
2. **PLAN.md** - Detailed execution plan outlining the steps and methodology
## Usage
### Prerequisites
- Docker (to run Neo4j)
- Python 3.6+
- Required packages: requests, beautifulsoup4, neo4j, pandas, python-levenshtein
### Setup
```bash
# Run Neo4j container
docker run -d --name neo4j-test -p 7687:7687 -p 7474:7474 -e NEO4J_AUTH=neo4j/password neo4j:latest
# Create virtual environment and install dependencies
python3 -m venv hospital_graph_env
source hospital_graph_env/bin/activate
pip install requests beautifulsoup4 neo4j pandas python-levenshtein
# Run the script
python build_hospital_graph.py
```
## Data Sources
1. **Hospital List**:
- Primary: NFZ registry or Ministry of Health (currently using sample data due to API unavailability)
- Fallback: Wikipedia's "Lista szpitali w Polsce"
2. **Hospital Staff**:
- Official hospital websites (often under "Zarząd" / "Rada Nadzorcza")
3. **Regional Councilors**:
- Biuletyn Informacji Publicznej (BIP) for each voivodeship
4. **Political Party Affiliation**:
- Same BIP pages or party registries
## Graph Schema
- **Hospital** node with properties: `name`, `city`, `website`, `nip`
- **Person** node with properties: `name`, `party`, `sourceUrls` (array)
- **Council** node with property: `region`
- Relationships:
- `[:DYREKTOR]` from Hospital to Person
- `[:RADNIA]` from Hospital to Person
- `[:RADNY]` from Person to Council (with `region` property)
- `[:CZŁONEK_PARTII]` from Person to Party (if applicable)
## Files Generated
1. **scrape_log.json** - Log of all scraping attempts
2. **overlap_report.csv** - Summary report of persons in multiple roles
3. **PLAN.md** - Detailed execution plan
## Validation
The script includes validation to ensure:
- Every hospital has at least one director relationship
- All person nodes have their source URLs properly recorded
- Graph schema constraints are enforced
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="GENERAL_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Binary file not shown.
File diff suppressed because it is too large Load Diff
+206
View File
@@ -0,0 +1,206 @@
name,party,sourceUrls,note
"Adam Dębski","","https://bip-spzoz-staugustow.podlaskie.eu/organy_wadze/kierownictwo_szpitala/","dyrektor od 1.09.2024"
"Adam Pietruczuk","","https://szpitalsokolka.pl/dyrekcja/",""
"Adam Szałanda","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
"Adam Łęczycki","Kww-Nasze-Podlasie","https://www.portalsamorzadowy.pl/powiat/bielski,295.html","radny powiatu"
"Agnieszka Kapczyńska","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
"Agnieszka Zaręba","","https://www.onkologia.bialystok.pl/pages/rada-spoleczna",""
"Agnieszka Zawistowska","","https://www.onkologia.bialystok.pl/pages/rada-spoleczna",""
"Albert Waldemar Litwinowicz","","Uchwała III/13/2024 Rady Powiatu Hajnowskiego","wójt Białowieży"
"Alicja Dobrowolska","Prawo i Sprawiedliwość","https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1","członek Zarządu Powiatu"
"Alicja Skindzielewska","","https://bip-sppzozc.podlaskie.eu/","p.o. Dyrektora"
"Andrzej Borusiewicz","","https://pl.wikipedia.org/wiki/Andrzej_Borusiewicz","prorektor MANS w Łomży, wiceprezes ARiMR"
"Andrzej Leszczyński","Kww-Nasze-Podlasie","https://www.portalsamorzadowy.pl/powiat/bielski,295.html","Przew. Rady Powiatu"
"Andrzej Parafiniuk","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
"Andrzej Skiepko","Kww-Ziemia-Hajnowska","https://www.portalsamorzadowy.pl/osoba/andrzej-skiepko,17764.html","Źródła rozbieżne: KWW Ziemia Hajnowska lub KWW Porozumienie Samorządowe"
"Andrzej Szewczuk","","https://bip-sppzozc.podlaskie.eu/|https://spzozsiemiatycze.pl/?page_id=395","dyrektor-elekt od VIII.2026"
"Andrzej Szymański","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
"Aneta Jolanta Szmidt","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
"Aneta Leonowicz","Kww-Narew-Odnowa","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3588266","wójt"
"Anita Krasińska","","https://www.szpitalkolno.pl/struktura/dyrekcja/","kier. OPS Grabowo"
"Anna Augustyn","Koalicja Obywatelska","https://www.onkologia.bialystok.pl/pages/rada-spoleczna","radna Sejmiku"
"Anna Kumiszcza","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
"Anna Malwina Szynkiewicz","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
"Anna Maria Gawlińska","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
"Anna Marta Aniśkiewicz","","https://www.radio.bialystok.pl/wiadomosci/index/id/228040","KWW Razem dla Sokółki"
"Anna Naszkiewicz","Koalicja Obywatelska","https://samorzad.podlaskie.eu/radni/naszkiewicz-anna.html","radna Sejmiku; b. wicemarszałek"
"Barbara Ciszewska","Komitet-2024-Niepotwierdzony","https://e-grajewo.pl/wiadomosc,barbara-ciszewska--kandydatka-na-burmistrza-grajewa,54754.html","b. radna powiatu; Stow. Nasza Przyszłość"
"Barbara Lubecka","","https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/","p.o. Gł. Księgowa"
"Bartosz Czaczkowski","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
"Bartosz Niebrzydowski","","https://uskbialystok.sisco.info/wladze,36_8",""
"Bartosz Rafał Ziemianowicz","","https://zambrowiacy.pl/info/bartosz-ziemianowicz/","b. radny miasta; historycznie PO; PO vs PiS"
"Beata Helena Muniak","","https://www.szpitalkolno.pl/struktura/dyrekcja/","Sekretarz Gminy"
"Bernadeta Krynicka","Prawo i Sprawiedliwość","https://pl.wikipedia.org/wiki/Bernadeta_Krynicka","radna Sejmiku; b. posłanka PiS"
"Bogdan Dyjuk","Polskie Stronnictwo Ludowe","https://www.onkologia.bialystok.pl/pages/rada-spoleczna|https://www.portalsamorzadowy.pl/osoba/bogdan-dyjuk,2177.html","radny Sejmiku; czł. Zarządu Woj."
"Bogdan Kalicki","","https://podlaskie.eu/zdrowie/bogdan-kalicki-oficjalnie-objal-stanowisko-dyrektora-sniadecji.html",""
"Bogdan Zieliński","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/osoba/bogdan-zielinski,4307.html","starosta, radny powiatu — NIEZWERYFIKOWANE czy przewodniczy"
"Bogusław Dębski","Prawica Rzeczypospolitej","https://bip-spzozwszjs.podlaskie.eu/rada.html","radny Sejmiku; historycznie PSL"
"Bolesław Ruszczyk","Prawo i Sprawiedliwość","https://bip.starostwograjewo.pl/612-kadencja-vii-2024-2029.html","radny powiatu"
"Bożena Jadwiga Chmielewska","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
"Bożena Łapińska","","https://bip-spspzozs.podlaskie.eu/",""
"Cezary Cieślukowski","Polskie Stronnictwo Ludowe","https://www.onkologia.bialystok.pl/pages/rada-spoleczna","Przewodniczący Sejmiku VII kad."
"Cezary Możejko","","https://szpitalsokolka.pl/dyrekcja/","możliwie wójt gm. Sidra; zbieżność nazwisk"
"Daniel Leszek Makarewicz","Kww-Suwalskie-Porozumienie-Prawicy","https://bip-spsws.podlaskie.eu/6b54de5acb7.html","NIEZWERYFIKOWANE"
"Daniel Szutko","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
"Dariusz Bogdan","","https://pl.wikipedia.org/wiki/Dariusz_Bogdan","b. wiceminister gospodarki; przynależność partyjna niepotwierdzona"
"Dariusz Domasiewicz","Kww-Przyjazna-Łomża","https://www.rynekzdrowia.pl/.../Nowy-dyrektor-w-Szpitalu-Wojewodzkim-w-Lomzy,269250,1.html","Radny Rady Miejskiej Łomży"
"Dariusz Jan Szkiłądź","Kww-Bau-2024","https://www.dziennikpowiatowy.pl/artykul/13664",""
"Dorota Iwanowicz","Trzecia Droga","https://www.farmer.pl/wybory/osoba/dorota-iwanowicz,221972.html|https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html","radna gm. Suwałki; NIEZWERYFIKOWANE"
"Dorota Waszkiewicz","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
"Edward Krasowski","Kww-Własny","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3739772","wójt; afiliacja niepotwierdzona"
"Eliza Kupich","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
"Elżbieta Baranowska","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
"Elżbieta Fionik","KW Koalicja Bielska","https://www.portalsamorzadowy.pl/powiat/bielski,295.html","radna powiatu"
"Elżbieta Luto","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
"Elżbieta Puczyłowska","Prawo i Sprawiedliwość","https://www.dziennikpowiatowy.pl/artykul/13664","wiczeprzewodnicząca Rady Powiatu"
"Elżbieta Sienkiewicz","","https://uskbialystok.sisco.info/wladze,36_8|https://szpitaldzieciecybialystok.sisco.info/?id=316",""
"Ewa Feszler","","https://www.onkologia.bialystok.pl/pages/rada-spoleczna",""
"Ewelina Cecotka-Białokozowicz","","https://bip-sppzozc.podlaskie.eu/",""
"Grzegorz Paweł Matys","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
"Grzegorz Tomaszuk","","https://spzozhajnowka.pl/strona-3343-dyrekcja.html",""
"Henryk Gryko","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
"Henryk Grzesiak","","https://uskbialystok.sisco.info/wladze,36_8",""
"Ireneusz Roman Kiendyś","Kww-Hajnówka-Razem-Bezpartyjny","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3459046","burmistrz"
"Irina Kowalska","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
"Jacek Niedźwiedzki","Koalicja Obywatelska","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html","poseł z Suwałk; NIEZWERYFIKOWANE"
"Jacek Piorunek","Koalicja Obywatelska","https://www.onkologia.bialystok.pl/pages/rada-spoleczna|https://www.portalsamorzadowy.pl/osoba/jacek-piorunek,6388.html","radny Sejmiku"
"Jadwiga Józefa Milewska","","https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1","NIEZWERYFIKOWANE"
"Jan Maksymilian Janiszewski","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
"Jan Niewiński","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/osoba/jan-niewinski,48951.html","radny powiatu"
"Jan Żeruń","Kww-Spw","https://siemiatycze.podlasie24.pl/region/zarzad-powiatu-siemiatyckiego-z-absolutorium-za-rok-",""
"Janusz Dzięcioł","","https://www.onkologia.bialystok.pl/pages/rada-spoleczna","inna osoba niż zmarły poseł PO; afiliacji nie potwierdzono"
"Janusz Józef Cieciórski","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
"Jarosław Gołubowski","Kww-Wspólna-Narewka","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3421407","wójt"
"Jarosław Matwiejuk","Lewica","https://bip-spzozwszjs.podlaskie.eu/rada.html","b. poseł SLD — NIEZWERYFIKOWANE"
"Jarosław Zygmunt Dworzański","Koalicja Obywatelska","https://bip-spzozwszjs.podlaskie.eu/rada.html|https://www.onkologia.bialystok.pl/pages/rada-spoleczna","radny Sejmiku; b. marszałek woj."
"Jerzy Jan Nikliński","","https://www.szpital-grajewo.pl/dyrekcja","NIEZWERYFIKOWANE"
"Jerzy Sirak","","https://www.portalsamorzadowy.pl/osoba/jerzy-sirak,1302.html","rozbieżne: KO vs historycznie SLD; b. burmistrz"
"Jerzy Wasiluk","Możliwie-Trzecia-Droga","Uchwała III/13/2024 Rady Powiatu Hajnowskiego","wójt Czyż; do potwierdzenia"
"Joanna Jędzul","Kww-Razem-Dla-Suwałk","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
"Joanna Kitlas","Kww-Bmn-Aktywny-Samorząd","https://www.portalsamorzadowy.pl/powiat/moniecki,362.html","NIEZWERYFIKOWANE"
"Joanna Sosnowska","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
"Justyna Drażba","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
"Justyna Jarmocik","Polskie Stronnictwo Ludowe","https://www.portalsamorzadowy.pl/powiat/hajnowski,350.html","Wicestarosta"
"Kamil Kowaleczko","","https://aleo.com/int/company/szpital-powiatowy-w-zambrowie","Prezes Zarządu od 16.07.2025"
"Karol Kossakowski","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna","kandydat do sejmiku; rej. Zambrów"
"Karol Pilecki","Koalicja Obywatelska","https://bip-spzozwszjs.podlaskie.eu/rada.html","radny Sejmiku; historycznie PSL"
"Katarzyna Kluczyk-Ceckowska","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
"Krystian Małyszko","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
"Krzysztof Pawłowski","Prawo i Sprawiedliwość","https://www.radio.bialystok.pl/wiadomosci/index/id/237569","radny powiatu, wiceprzew. Rady Powiatu"
"Krzysztof Rodzik","","https://aleo.com/int/company/szpital-powiatowy-w-zambrowie",""
"Krzysztof Ryszard Kozicki","Prawo i Sprawiedliwość","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3469307","wójt Piątnicy"
"Krzysztof Turowski","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
"Lech Marek Szabłowski","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/osoba/lech-szablowski,55269.html","starosta łomżyński / radny powiatu"
"Lech Rutkowski","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
"Lech Zenon Hackiewicz","","https://spzozsiemiatycze.pl/?page_id=395",""
"Leon Małaszewski","Kww-Ziemia-Hajnowska","Uchwała III/13/2024 Rady Powiatu Hajnowskiego","wójt; niepotwierdzone partyjnie"
"Leszek Maciej Lulewicz","","https://www.onkologia.bialystok.pl/pages/rada-spoleczna",""
"Lucyna Smoktunowicz","Kww-Nasza-Przyszłość-Bezpartyjna","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3412858","wójt"
"Maciej Paweł Bednarko","Kww-Macieja-Pawła-Bednarko","https://www.grajewo.pl/pl/aktualnosci/maciej-pawel-bednarko-zaprzysiezony...","burmistrz Grajewa"
"Maciej Plesiewicz","Kw-Ziemia-Sejneńska","https://www.suwalki24.pl/article/63,powiat-sejnenski-maciej-plesiewicz-nadal-starosta...","starosta; NIEZWERYFIKOWANE czy przewodniczy"
"Maciej Żywno","Polska 2050","https://www.senat.gov.pl/sklad/senatorowie/senator,1072,11,maciej-zywno.html","senator, wicemarszałek Senatu"
"Marcin Biedrzycki","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
"Marcin Borsuk","","https://uskbialystok.sisco.info/wladze,36_8",""
"Marek Dobkowski","Kww-Samorząd-Powiatu-Augustowskiego","https://www.dziennikpowiatowy.pl/artykul/13664",""
"Marek Kostrzewski","Kww-Dobry-Powiat","https://bip.starostwograjewo.pl/612-kadencja-vii-2024-2029.html","radny powiatu"
"Marek Malinowski","Wicemarszałek-Woj-Koalicja-Obywatelska-Niezrzeszony","https://uskbialystok.sisco.info/wladze,36_8","do potwierdzenia"
"Marek Olbryś","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/osoba/marek-olbrys,876.html","wicemarszałek woj. / radny Sejmiku"
"Marek Samul","Koalicja Obywatelska","https://www.szpitalkolno.pl/struktura/dyrekcja/","b. starosta kolneński; NIEZWERYFIKOWANE"
"Marek Stanisław Karp","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna","p.o. od 20.02.2024"
"Marek Wasilewski","","https://bip-spspzozs.podlaskie.eu/",""
"Maria Julita Budrowska","","https://szpitalsokolka.pl/dyrekcja/",""
"Maria Konopka","Koalicja Obywatelska","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna","kandydatka PO do sejmiku 2024"
"Maria Ryżyk","KW Koalicja Bielska","https://www.portalsamorzadowy.pl/powiat/bielski,295.html","radna powiatu"
"Mariusz Cieślik","Polskie Stronnictwo Ludowe","https://www.radio.bialystok.pl/wiadomosci/index/id/238459","wybrany z KWW Ruch Ludowy"
"Mariusz Pyzowski","Koalicja Obywatelska","https://www.portalsamorzadowy.pl/osoba/mariusz-pyzowski,34169.html","Przew. Rady Powiatu"
"Mariusz Rakowski","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/osoba/mariusz-rakowski,106463.html","radny Rady Miasta Kolno"
"Marta Żybura","Koalicja Obywatelska","https://bip-spsws.podlaskie.eu/6b54de5acb7.html","kandydatka KO do Rady Miejskiej 2024; NIEZWERYFIKOWANE"
"Michał Kulczewski","Kww-Porozumienie-Samorządowe-Powiatu-Kolno","https://www.portalsamorzadowy.pl/osoba/michal-kulczewski,57069.html","radny powiatu"
"Michał Kulesza","","https://aleo.com/int/company/szpital-powiatowy-w-zambrowie",""
"Milena Formejster","","https://www.szpital-grajewo.pl/dyrekcja",""
"Milena Siok","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna","NIEZWERYFIKOWANE"
"Mirosław Markiewicz","Kww-Roztropna-Troska-O-Dobro-Gminy","https://w.radio.bialystok.pl/wiadomosci/index/id/236847","burmistrz Kleszczele"
"Monika Miklaszewicz","","https://bip-sppzozc.podlaskie.eu/",""
"Natalia Bogdan-Czarnowska","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
"Patrycja Magdalena Bugaj","","https://aleo.com/int/company/szpital-powiatowy-w-zambrowie",""
"Paweł Jamróz","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
"Piotr Bożko","KW Koalicja Bielska","https://samorzad.gov.pl/web/powiat-bielski/ii-sesja-rady-powiatu-w-bielsku-podlaskim","wicestarosta, radny powiatu"
"Piotr Rećko","Prawo i Sprawiedliwość","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/rada_powiatu/kandydat/3296821","starosta sokólski, radny powiatu"
"Piotr Rusiecki","Prawo i Sprawiedliwość","https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1","starosta augustowski, radny powiatu"
"Piotr Selwesiuk","KW Koalicja Bielska","https://www.spzoz-bielsk.pl/o-nas/dyrekcja/","wypowiedzenie z 29.06.2026"
"Piotr Siniakowicz","Koalicja Obywatelska","https://pl.wikipedia.org/wiki/Piotr_Siniakowicz","KWW Dla Siemiatycz Razem"
"Rafał Kołakowski","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
"Renata Szydłowska","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
"Robert Jabłoński","Prawo i Sprawiedliwość","https://www.onkologia.bialystok.pl/pages/rada-spoleczna","prawdopodobnie PiS; NIEZWERYFIKOWANE"
"Robert Wacław Nadara","Kww-Skuteczny-I-Przyjazny-Samorząd","https://www.radio.bialystok.pl/wiadomosci/index/id/237569","radny powiatu"
"Stanisław Kossakowski","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
"Sylwester Zgierun","Polskie Stronnictwo Ludowe","https://siemiatycze.podlasie24.pl/region/zarzad-powiatu-siemiatyckiego-z-absolutorium-za-rok-2023-20240615102143","KWW Ruch Ludowy"
"Sławomir Jan Szeszko","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
"Tadeusz Buchowiecki","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html","NIEZWERYFIKOWANE"
"Tadeusz Klama","Kww-Porozumienie-Samorządowe-Powiatu-Kolno","https://www.portalsamorzadowy.pl/osoba/tadeusz-klama,58658.html","radny powiatu"
"Tomasz Musiuk","","https://spzozhajnowka.pl/strona-3343-dyrekcja.html",""
"Tomasz Romualdowski","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
"Tomasz Stefan Madras","Prawo i Sprawiedliwość","https://bip-spzozwszjs.podlaskie.eu/rada.html","radny Sejmiku; b. wicewojewoda"
"Urszula Ordowska","","https://szpital.sejny.pl/kadra",""
"Waldemar Jedliński","Prawo i Sprawiedliwość","https://www.dziennikpowiatowy.pl/artykul/13664","członek Zarządu Powiatu"
"Waldemar Kwaterski","Koalicja Obywatelska","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html|https://www.radio.bialystok.pl/gosc/suwalki/id/202941","radny Sejmiku; dyr. SP ZOZ Sejny; NIEZWERYFIKOWANE"
"Waldemar Remfeld","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/powiat/grajewski,329.html","starosta grajewski, radny powiatu"
"Walentyna Tyszko-Dubow","","https://samorzad.gov.pl/web/powiat-bielski/ii-sesja-rady-powiatu-w-bielsku-podlaskim","PO/KO z Hajnówki"
"Wioletta Tomaszycka-Bednarczyk","","https://bip-spzoz-staugustow.podlaskie.eu/organy_wadze/kierownictwo_szpitala/",""
"Wojciech Strzałkowski","","https://bip-spzozwszjs.podlaskie.eu/rada.html","NIEZWERYFIKOWANE"
"Wojciech Wasilewski","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html","NIEZWERYFIKOWANE"
"Wojciech Łuczaj","","https://bip-spzozwszjs.podlaskie.eu/rada.html","możliwie Łuczaj-Pogorzelski, PSL — NIEZWERYFIKOWANE"
"Władysław Renowicki","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
"Zbigniew Jan Szpakowski","Kww-Nasze-Podlasie","https://www.portalsamorzadowy.pl/powiat/bielski,295.html","radny powiatu, czł. Zarządu"
"dr hab. Joanna Reszeć-Giełażyn","","https://uskbialystok.sisco.info/wladze,36_8",""
"dr hab. n. med. Marzena Wojewódzka-Żelezniakowicz","","https://uskwb.pl/dyrekcja-szpitala/",""
"dr n. med. Anatol Aksiucik","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
"dr n. med. Arsalan Azzaddin","","https://www.spzoz-bielsk.pl/o-nas/dyrekcja/",""
"dr n. med. Beata Olejnik","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
"dr n. med. Dorota Elżbieta Kazberuk","","https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html",""
"dr n. med. Nadzieja Sołowiej","","https://bip-sppzozc.podlaskie.eu/",""
"dr n. med. Ryta Filipkowska","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
"dr n. o zdr. Cezary Rzemek","","https://szpitaldzieciecybialystok.sisco.info/?id=316",""
"dr n. o zdr. Elżbieta Potentas","","https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html",""
"dr n. o zdr. Magdalena Joanna Borkowska","","https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html",""
"lek. Julita Nikołajuk-Stasiuk","","https://szpitalwysmaz.pl/pl/onas",""
"lek. Justyna Anna Piłasiewicz","","https://bip-sppzozc.podlaskie.eu/",""
"lek. Michał Naczas","","https://bip-spspzozs.podlaskie.eu/",""
"lek. Oksana Dowlaszewicz","","https://www.szpitalkolno.pl/struktura/dyrekcja/","p.o. Z-ca ds. Lecznictwa"
"lek. Włodzimierz Żaworonok","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
"lek. med. Adam Leszczyński","","https://www.szpital-grajewo.pl/dyrekcja","p.o. Z-ca ds. Lecznictwa"
"lek. med. Sebastian Wysocki","","https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/",""
"lek. med. Łucja Chojnowska","","https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/","p.o. Z-ca ds. Lecznictwa"
"mgr Aleksandra Łapiak","","https://szpitalwysmaz.pl/pl/onas",""
"mgr Alicja Urbańska","","https://www.szpitalkolno.pl/struktura/dyrekcja/",""
"mgr Aneta Długozima","","https://www.szpitalkolno.pl/struktura/dyrekcja/",""
"mgr Anna Wądołkowska","","https://szpitalwysmaz.pl/pl/onas",""
"mgr Beata Kropiewnicka","","https://uskwb.pl/dyrekcja-szpitala/",""
"mgr Beata Szyszkowska","","https://www.szpital-grajewo.pl/dyrekcja",""
"mgr Bożena Kostro","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
"mgr Halina Skibicka","","https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/",""
"mgr Izabela Górska","","https://www.szpitalkolno.pl/struktura/dyrekcja/","p.o. Z-ca ds. Ekon.-Fin. / Gł. Księgowy"
"mgr Katarzyna Bardłowska","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
"mgr Marta Romanowska","","https://www.szpital-grajewo.pl/dyrekcja",""
"mgr Małgorzata Zdanewicz","","https://www.szpital-grajewo.pl/dyrekcja",""
"mgr Monika Anna Borkowska","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
"mgr Piotr Kołos","","https://uskwb.pl/dyrekcja-szpitala/",""
"mgr Sylwia Ignaczak-Piotrowska","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
"mgr Wojciech Łuczaj","Prawo i Sprawiedliwość","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3739309","kandydat na burmistrza Czyżewa 2024 z KW PiS"
"mgr inż. Sławomir Wilczewski","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
"mgr inż. Wojciech Roszkowski","","https://szpitaldzieciecybialystok.sisco.info/?id=316",""
"ppłk Małgorzata Błażewicz","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
"prof. dr hab. Dariusz Lebensztejn","","https://uskbialystok.sisco.info/wladze,36_8",""
"prof. dr hab. Jerzy Pałka","","https://uskbialystok.sisco.info/wladze,36_8",""
"prof. dr hab. Sławomir Terlikowski","","https://uskbialystok.sisco.info/wladze,36_8",""
"prof. dr hab. Wojciech Miltyk","","https://uskbialystok.sisco.info/wladze,36_8",""
"prof. dr hab. n. med. Andrzej Dąbrowski","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
"prof. dr hab. n. med. Anna Niemcunowicz-Janica","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
"prof. dr hab. n. med. Anna Wasilewska","","https://szpitaldzieciecybialystok.sisco.info/?id=316",""
"prof. dr hab. n. med. Beata Naumnik","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
"prof. dr hab. n. med. Elżbieta Tryniszewska","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
"prof. dr hab. n. med. Halina Car","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
"prof. dr hab. n. med. Jan Kochanowicz","","https://uskwb.pl/dyrekcja-szpitala/",""
"prof. dr hab. n. med. Jarosław Daniluk","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
"płk SG Marek Sochański","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
"st. bryg. Marcin Wierel","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
1 name party sourceUrls note
2 Adam Dębski https://bip-spzoz-staugustow.podlaskie.eu/organy_wadze/kierownictwo_szpitala/ dyrektor od 1.09.2024
3 Adam Pietruczuk https://szpitalsokolka.pl/dyrekcja/
4 Adam Szałanda https://bip-spsws.podlaskie.eu/6b54de5acb7.html
5 Adam Łęczycki Kww-Nasze-Podlasie https://www.portalsamorzadowy.pl/powiat/bielski,295.html radny powiatu
6 Agnieszka Kapczyńska https://bip-spsws.podlaskie.eu/6b54de5acb7.html
7 Agnieszka Zaręba https://www.onkologia.bialystok.pl/pages/rada-spoleczna
8 Agnieszka Zawistowska https://www.onkologia.bialystok.pl/pages/rada-spoleczna
9 Albert Waldemar Litwinowicz Uchwała III/13/2024 Rady Powiatu Hajnowskiego wójt Białowieży
10 Alicja Dobrowolska Prawo i Sprawiedliwość https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1 członek Zarządu Powiatu
11 Alicja Skindzielewska https://bip-sppzozc.podlaskie.eu/ p.o. Dyrektora
12 Andrzej Borusiewicz https://pl.wikipedia.org/wiki/Andrzej_Borusiewicz prorektor MANS w Łomży, wiceprezes ARiMR
13 Andrzej Leszczyński Kww-Nasze-Podlasie https://www.portalsamorzadowy.pl/powiat/bielski,295.html Przew. Rady Powiatu
14 Andrzej Parafiniuk https://bip-spzozwszjs.podlaskie.eu/rada.html
15 Andrzej Skiepko Kww-Ziemia-Hajnowska https://www.portalsamorzadowy.pl/osoba/andrzej-skiepko,17764.html Źródła rozbieżne: KWW Ziemia Hajnowska lub KWW Porozumienie Samorządowe
16 Andrzej Szewczuk https://bip-sppzozc.podlaskie.eu/|https://spzozsiemiatycze.pl/?page_id=395 dyrektor-elekt od VIII.2026
17 Andrzej Szymański https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna
18 Aneta Jolanta Szmidt https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html
19 Aneta Leonowicz Kww-Narew-Odnowa https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3588266 wójt
20 Anita Krasińska https://www.szpitalkolno.pl/struktura/dyrekcja/ kier. OPS Grabowo
21 Anna Augustyn Koalicja Obywatelska https://www.onkologia.bialystok.pl/pages/rada-spoleczna radna Sejmiku
22 Anna Kumiszcza https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html
23 Anna Malwina Szynkiewicz https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna
24 Anna Maria Gawlińska https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html
25 Anna Marta Aniśkiewicz https://www.radio.bialystok.pl/wiadomosci/index/id/228040 KWW Razem dla Sokółki
26 Anna Naszkiewicz Koalicja Obywatelska https://samorzad.podlaskie.eu/radni/naszkiewicz-anna.html radna Sejmiku; b. wicemarszałek
27 Barbara Ciszewska Komitet-2024-Niepotwierdzony https://e-grajewo.pl/wiadomosc,barbara-ciszewska--kandydatka-na-burmistrza-grajewa,54754.html b. radna powiatu; Stow. Nasza Przyszłość
28 Barbara Lubecka https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/ p.o. Gł. Księgowa
29 Bartosz Czaczkowski https://bip-spzozwszjs.podlaskie.eu/rada.html
30 Bartosz Niebrzydowski https://uskbialystok.sisco.info/wladze,36_8
31 Bartosz Rafał Ziemianowicz https://zambrowiacy.pl/info/bartosz-ziemianowicz/ b. radny miasta; historycznie PO; PO vs PiS
32 Beata Helena Muniak https://www.szpitalkolno.pl/struktura/dyrekcja/ Sekretarz Gminy
33 Bernadeta Krynicka Prawo i Sprawiedliwość https://pl.wikipedia.org/wiki/Bernadeta_Krynicka radna Sejmiku; b. posłanka PiS
34 Bogdan Dyjuk Polskie Stronnictwo Ludowe https://www.onkologia.bialystok.pl/pages/rada-spoleczna|https://www.portalsamorzadowy.pl/osoba/bogdan-dyjuk,2177.html radny Sejmiku; czł. Zarządu Woj.
35 Bogdan Kalicki https://podlaskie.eu/zdrowie/bogdan-kalicki-oficjalnie-objal-stanowisko-dyrektora-sniadecji.html
36 Bogdan Zieliński Prawo i Sprawiedliwość https://www.portalsamorzadowy.pl/osoba/bogdan-zielinski,4307.html starosta, radny powiatu — NIEZWERYFIKOWANE czy przewodniczy
37 Bogusław Dębski Prawica Rzeczypospolitej https://bip-spzozwszjs.podlaskie.eu/rada.html radny Sejmiku; historycznie PSL
38 Bolesław Ruszczyk Prawo i Sprawiedliwość https://bip.starostwograjewo.pl/612-kadencja-vii-2024-2029.html radny powiatu
39 Bożena Jadwiga Chmielewska https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html
40 Bożena Łapińska https://bip-spspzozs.podlaskie.eu/
41 Cezary Cieślukowski Polskie Stronnictwo Ludowe https://www.onkologia.bialystok.pl/pages/rada-spoleczna Przewodniczący Sejmiku VII kad.
42 Cezary Możejko https://szpitalsokolka.pl/dyrekcja/ możliwie wójt gm. Sidra; zbieżność nazwisk
43 Daniel Leszek Makarewicz Kww-Suwalskie-Porozumienie-Prawicy https://bip-spsws.podlaskie.eu/6b54de5acb7.html NIEZWERYFIKOWANE
44 Daniel Szutko https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/
45 Dariusz Bogdan https://pl.wikipedia.org/wiki/Dariusz_Bogdan b. wiceminister gospodarki; przynależność partyjna niepotwierdzona
46 Dariusz Domasiewicz Kww-Przyjazna-Łomża https://www.rynekzdrowia.pl/.../Nowy-dyrektor-w-Szpitalu-Wojewodzkim-w-Lomzy,269250,1.html Radny Rady Miejskiej Łomży
47 Dariusz Jan Szkiłądź Kww-Bau-2024 https://www.dziennikpowiatowy.pl/artykul/13664
48 Dorota Iwanowicz Trzecia Droga https://www.farmer.pl/wybory/osoba/dorota-iwanowicz,221972.html|https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html radna gm. Suwałki; NIEZWERYFIKOWANE
49 Dorota Waszkiewicz https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna
50 Edward Krasowski Kww-Własny https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3739772 wójt; afiliacja niepotwierdzona
51 Eliza Kupich https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html
52 Elżbieta Baranowska https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html
53 Elżbieta Fionik KW Koalicja Bielska https://www.portalsamorzadowy.pl/powiat/bielski,295.html radna powiatu
54 Elżbieta Luto https://bip-spsws.podlaskie.eu/6b54de5acb7.html
55 Elżbieta Puczyłowska Prawo i Sprawiedliwość https://www.dziennikpowiatowy.pl/artykul/13664 wiczeprzewodnicząca Rady Powiatu
56 Elżbieta Sienkiewicz https://uskbialystok.sisco.info/wladze,36_8|https://szpitaldzieciecybialystok.sisco.info/?id=316
57 Ewa Feszler https://www.onkologia.bialystok.pl/pages/rada-spoleczna
58 Ewelina Cecotka-Białokozowicz https://bip-sppzozc.podlaskie.eu/
59 Grzegorz Paweł Matys https://bip-spsws.podlaskie.eu/6b54de5acb7.html
60 Grzegorz Tomaszuk https://spzozhajnowka.pl/strona-3343-dyrekcja.html
61 Henryk Gryko https://bip-spzozwszjs.podlaskie.eu/rada.html
62 Henryk Grzesiak https://uskbialystok.sisco.info/wladze,36_8
63 Ireneusz Roman Kiendyś Kww-Hajnówka-Razem-Bezpartyjny https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3459046 burmistrz
64 Irina Kowalska https://bip-spzozwszjs.podlaskie.eu/rada.html
65 Jacek Niedźwiedzki Koalicja Obywatelska https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html poseł z Suwałk; NIEZWERYFIKOWANE
66 Jacek Piorunek Koalicja Obywatelska https://www.onkologia.bialystok.pl/pages/rada-spoleczna|https://www.portalsamorzadowy.pl/osoba/jacek-piorunek,6388.html radny Sejmiku
67 Jadwiga Józefa Milewska https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1 NIEZWERYFIKOWANE
68 Jan Maksymilian Janiszewski https://bip-spzozwszjs.podlaskie.eu/rada.html
69 Jan Niewiński Prawo i Sprawiedliwość https://www.portalsamorzadowy.pl/osoba/jan-niewinski,48951.html radny powiatu
70 Jan Żeruń Kww-Spw https://siemiatycze.podlasie24.pl/region/zarzad-powiatu-siemiatyckiego-z-absolutorium-za-rok-
71 Janusz Dzięcioł https://www.onkologia.bialystok.pl/pages/rada-spoleczna inna osoba niż zmarły poseł PO; afiliacji nie potwierdzono
72 Janusz Józef Cieciórski https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna
73 Jarosław Gołubowski Kww-Wspólna-Narewka https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3421407 wójt
74 Jarosław Matwiejuk Lewica https://bip-spzozwszjs.podlaskie.eu/rada.html b. poseł SLD — NIEZWERYFIKOWANE
75 Jarosław Zygmunt Dworzański Koalicja Obywatelska https://bip-spzozwszjs.podlaskie.eu/rada.html|https://www.onkologia.bialystok.pl/pages/rada-spoleczna radny Sejmiku; b. marszałek woj.
76 Jerzy Jan Nikliński https://www.szpital-grajewo.pl/dyrekcja NIEZWERYFIKOWANE
77 Jerzy Sirak https://www.portalsamorzadowy.pl/osoba/jerzy-sirak,1302.html rozbieżne: KO vs historycznie SLD; b. burmistrz
78 Jerzy Wasiluk Możliwie-Trzecia-Droga Uchwała III/13/2024 Rady Powiatu Hajnowskiego wójt Czyż; do potwierdzenia
79 Joanna Jędzul Kww-Razem-Dla-Suwałk https://bip-spsws.podlaskie.eu/6b54de5acb7.html
80 Joanna Kitlas Kww-Bmn-Aktywny-Samorząd https://www.portalsamorzadowy.pl/powiat/moniecki,362.html NIEZWERYFIKOWANE
81 Joanna Sosnowska https://bip-spzozwszjs.podlaskie.eu/rada.html
82 Justyna Drażba https://bip-spsws.podlaskie.eu/6b54de5acb7.html
83 Justyna Jarmocik Polskie Stronnictwo Ludowe https://www.portalsamorzadowy.pl/powiat/hajnowski,350.html Wicestarosta
84 Kamil Kowaleczko https://aleo.com/int/company/szpital-powiatowy-w-zambrowie Prezes Zarządu od 16.07.2025
85 Karol Kossakowski https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna kandydat do sejmiku; rej. Zambrów
86 Karol Pilecki Koalicja Obywatelska https://bip-spzozwszjs.podlaskie.eu/rada.html radny Sejmiku; historycznie PSL
87 Katarzyna Kluczyk-Ceckowska https://bip-spzozwszjs.podlaskie.eu/rada.html
88 Krystian Małyszko https://bip-spsws.podlaskie.eu/6b54de5acb7.html
89 Krzysztof Pawłowski Prawo i Sprawiedliwość https://www.radio.bialystok.pl/wiadomosci/index/id/237569 radny powiatu, wiceprzew. Rady Powiatu
90 Krzysztof Rodzik https://aleo.com/int/company/szpital-powiatowy-w-zambrowie
91 Krzysztof Ryszard Kozicki Prawo i Sprawiedliwość https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3469307 wójt Piątnicy
92 Krzysztof Turowski https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/
93 Lech Marek Szabłowski Prawo i Sprawiedliwość https://www.portalsamorzadowy.pl/osoba/lech-szablowski,55269.html starosta łomżyński / radny powiatu
94 Lech Rutkowski https://bip-spzozwszjs.podlaskie.eu/rada.html
95 Lech Zenon Hackiewicz https://spzozsiemiatycze.pl/?page_id=395
96 Leon Małaszewski Kww-Ziemia-Hajnowska Uchwała III/13/2024 Rady Powiatu Hajnowskiego wójt; niepotwierdzone partyjnie
97 Leszek Maciej Lulewicz https://www.onkologia.bialystok.pl/pages/rada-spoleczna
98 Lucyna Smoktunowicz Kww-Nasza-Przyszłość-Bezpartyjna https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3412858 wójt
99 Maciej Paweł Bednarko Kww-Macieja-Pawła-Bednarko https://www.grajewo.pl/pl/aktualnosci/maciej-pawel-bednarko-zaprzysiezony... burmistrz Grajewa
100 Maciej Plesiewicz Kw-Ziemia-Sejneńska https://www.suwalki24.pl/article/63,powiat-sejnenski-maciej-plesiewicz-nadal-starosta... starosta; NIEZWERYFIKOWANE czy przewodniczy
101 Maciej Żywno Polska 2050 https://www.senat.gov.pl/sklad/senatorowie/senator,1072,11,maciej-zywno.html senator, wicemarszałek Senatu
102 Marcin Biedrzycki https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna
103 Marcin Borsuk https://uskbialystok.sisco.info/wladze,36_8
104 Marek Dobkowski Kww-Samorząd-Powiatu-Augustowskiego https://www.dziennikpowiatowy.pl/artykul/13664
105 Marek Kostrzewski Kww-Dobry-Powiat https://bip.starostwograjewo.pl/612-kadencja-vii-2024-2029.html radny powiatu
106 Marek Malinowski Wicemarszałek-Woj-Koalicja-Obywatelska-Niezrzeszony https://uskbialystok.sisco.info/wladze,36_8 do potwierdzenia
107 Marek Olbryś Prawo i Sprawiedliwość https://www.portalsamorzadowy.pl/osoba/marek-olbrys,876.html wicemarszałek woj. / radny Sejmiku
108 Marek Samul Koalicja Obywatelska https://www.szpitalkolno.pl/struktura/dyrekcja/ b. starosta kolneński; NIEZWERYFIKOWANE
109 Marek Stanisław Karp https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna p.o. od 20.02.2024
110 Marek Wasilewski https://bip-spspzozs.podlaskie.eu/
111 Maria Julita Budrowska https://szpitalsokolka.pl/dyrekcja/
112 Maria Konopka Koalicja Obywatelska https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna kandydatka PO do sejmiku 2024
113 Maria Ryżyk KW Koalicja Bielska https://www.portalsamorzadowy.pl/powiat/bielski,295.html radna powiatu
114 Mariusz Cieślik Polskie Stronnictwo Ludowe https://www.radio.bialystok.pl/wiadomosci/index/id/238459 wybrany z KWW Ruch Ludowy
115 Mariusz Pyzowski Koalicja Obywatelska https://www.portalsamorzadowy.pl/osoba/mariusz-pyzowski,34169.html Przew. Rady Powiatu
116 Mariusz Rakowski Prawo i Sprawiedliwość https://www.portalsamorzadowy.pl/osoba/mariusz-rakowski,106463.html radny Rady Miasta Kolno
117 Marta Żybura Koalicja Obywatelska https://bip-spsws.podlaskie.eu/6b54de5acb7.html kandydatka KO do Rady Miejskiej 2024; NIEZWERYFIKOWANE
118 Michał Kulczewski Kww-Porozumienie-Samorządowe-Powiatu-Kolno https://www.portalsamorzadowy.pl/osoba/michal-kulczewski,57069.html radny powiatu
119 Michał Kulesza https://aleo.com/int/company/szpital-powiatowy-w-zambrowie
120 Milena Formejster https://www.szpital-grajewo.pl/dyrekcja
121 Milena Siok https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna NIEZWERYFIKOWANE
122 Mirosław Markiewicz Kww-Roztropna-Troska-O-Dobro-Gminy https://w.radio.bialystok.pl/wiadomosci/index/id/236847 burmistrz Kleszczele
123 Monika Miklaszewicz https://bip-sppzozc.podlaskie.eu/
124 Natalia Bogdan-Czarnowska https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html
125 Patrycja Magdalena Bugaj https://aleo.com/int/company/szpital-powiatowy-w-zambrowie
126 Paweł Jamróz https://bip-spzozwszjs.podlaskie.eu/rada.html
127 Piotr Bożko KW Koalicja Bielska https://samorzad.gov.pl/web/powiat-bielski/ii-sesja-rady-powiatu-w-bielsku-podlaskim wicestarosta, radny powiatu
128 Piotr Rećko Prawo i Sprawiedliwość https://samorzad2024.pkw.gov.pl/samorzad2024/pl/rada_powiatu/kandydat/3296821 starosta sokólski, radny powiatu
129 Piotr Rusiecki Prawo i Sprawiedliwość https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1 starosta augustowski, radny powiatu
130 Piotr Selwesiuk KW Koalicja Bielska https://www.spzoz-bielsk.pl/o-nas/dyrekcja/ wypowiedzenie z 29.06.2026
131 Piotr Siniakowicz Koalicja Obywatelska https://pl.wikipedia.org/wiki/Piotr_Siniakowicz KWW Dla Siemiatycz Razem
132 Rafał Kołakowski https://bip-spzozwszjs.podlaskie.eu/rada.html
133 Renata Szydłowska https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna
134 Robert Jabłoński Prawo i Sprawiedliwość https://www.onkologia.bialystok.pl/pages/rada-spoleczna prawdopodobnie PiS; NIEZWERYFIKOWANE
135 Robert Wacław Nadara Kww-Skuteczny-I-Przyjazny-Samorząd https://www.radio.bialystok.pl/wiadomosci/index/id/237569 radny powiatu
136 Stanisław Kossakowski https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna
137 Sylwester Zgierun Polskie Stronnictwo Ludowe https://siemiatycze.podlasie24.pl/region/zarzad-powiatu-siemiatyckiego-z-absolutorium-za-rok-2023-20240615102143 KWW Ruch Ludowy
138 Sławomir Jan Szeszko https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html
139 Tadeusz Buchowiecki https://bip-spsws.podlaskie.eu/6b54de5acb7.html NIEZWERYFIKOWANE
140 Tadeusz Klama Kww-Porozumienie-Samorządowe-Powiatu-Kolno https://www.portalsamorzadowy.pl/osoba/tadeusz-klama,58658.html radny powiatu
141 Tomasz Musiuk https://spzozhajnowka.pl/strona-3343-dyrekcja.html
142 Tomasz Romualdowski https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html
143 Tomasz Stefan Madras Prawo i Sprawiedliwość https://bip-spzozwszjs.podlaskie.eu/rada.html radny Sejmiku; b. wicewojewoda
144 Urszula Ordowska https://szpital.sejny.pl/kadra
145 Waldemar Jedliński Prawo i Sprawiedliwość https://www.dziennikpowiatowy.pl/artykul/13664 członek Zarządu Powiatu
146 Waldemar Kwaterski Koalicja Obywatelska https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html|https://www.radio.bialystok.pl/gosc/suwalki/id/202941 radny Sejmiku; dyr. SP ZOZ Sejny; NIEZWERYFIKOWANE
147 Waldemar Remfeld Prawo i Sprawiedliwość https://www.portalsamorzadowy.pl/powiat/grajewski,329.html starosta grajewski, radny powiatu
148 Walentyna Tyszko-Dubow https://samorzad.gov.pl/web/powiat-bielski/ii-sesja-rady-powiatu-w-bielsku-podlaskim PO/KO z Hajnówki
149 Wioletta Tomaszycka-Bednarczyk https://bip-spzoz-staugustow.podlaskie.eu/organy_wadze/kierownictwo_szpitala/
150 Wojciech Strzałkowski https://bip-spzozwszjs.podlaskie.eu/rada.html NIEZWERYFIKOWANE
151 Wojciech Wasilewski https://bip-spsws.podlaskie.eu/6b54de5acb7.html NIEZWERYFIKOWANE
152 Wojciech Łuczaj https://bip-spzozwszjs.podlaskie.eu/rada.html możliwie Łuczaj-Pogorzelski, PSL — NIEZWERYFIKOWANE
153 Władysław Renowicki https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html
154 Zbigniew Jan Szpakowski Kww-Nasze-Podlasie https://www.portalsamorzadowy.pl/powiat/bielski,295.html radny powiatu, czł. Zarządu
155 dr hab. Joanna Reszeć-Giełażyn https://uskbialystok.sisco.info/wladze,36_8
156 dr hab. n. med. Marzena Wojewódzka-Żelezniakowicz https://uskwb.pl/dyrekcja-szpitala/
157 dr n. med. Anatol Aksiucik https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/
158 dr n. med. Arsalan Azzaddin https://www.spzoz-bielsk.pl/o-nas/dyrekcja/
159 dr n. med. Beata Olejnik https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/
160 dr n. med. Dorota Elżbieta Kazberuk https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html
161 dr n. med. Nadzieja Sołowiej https://bip-sppzozc.podlaskie.eu/
162 dr n. med. Ryta Filipkowska https://szpitaldzieciecybialystok.sisco.info/?id=416
163 dr n. o zdr. Cezary Rzemek https://szpitaldzieciecybialystok.sisco.info/?id=316
164 dr n. o zdr. Elżbieta Potentas https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html
165 dr n. o zdr. Magdalena Joanna Borkowska https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html
166 lek. Julita Nikołajuk-Stasiuk https://szpitalwysmaz.pl/pl/onas
167 lek. Justyna Anna Piłasiewicz https://bip-sppzozc.podlaskie.eu/
168 lek. Michał Naczas https://bip-spspzozs.podlaskie.eu/
169 lek. Oksana Dowlaszewicz https://www.szpitalkolno.pl/struktura/dyrekcja/ p.o. Z-ca ds. Lecznictwa
170 lek. Włodzimierz Żaworonok https://bip-spsws.podlaskie.eu/6b54de5acb7.html
171 lek. med. Adam Leszczyński https://www.szpital-grajewo.pl/dyrekcja p.o. Z-ca ds. Lecznictwa
172 lek. med. Sebastian Wysocki https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/
173 lek. med. Łucja Chojnowska https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/ p.o. Z-ca ds. Lecznictwa
174 mgr Aleksandra Łapiak https://szpitalwysmaz.pl/pl/onas
175 mgr Alicja Urbańska https://www.szpitalkolno.pl/struktura/dyrekcja/
176 mgr Aneta Długozima https://www.szpitalkolno.pl/struktura/dyrekcja/
177 mgr Anna Wądołkowska https://szpitalwysmaz.pl/pl/onas
178 mgr Beata Kropiewnicka https://uskwb.pl/dyrekcja-szpitala/
179 mgr Beata Szyszkowska https://www.szpital-grajewo.pl/dyrekcja
180 mgr Bożena Kostro https://szpitaldzieciecybialystok.sisco.info/?id=416
181 mgr Halina Skibicka https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/
182 mgr Izabela Górska https://www.szpitalkolno.pl/struktura/dyrekcja/ p.o. Z-ca ds. Ekon.-Fin. / Gł. Księgowy
183 mgr Katarzyna Bardłowska https://szpitaldzieciecybialystok.sisco.info/?id=416
184 mgr Marta Romanowska https://www.szpital-grajewo.pl/dyrekcja
185 mgr Małgorzata Zdanewicz https://www.szpital-grajewo.pl/dyrekcja
186 mgr Monika Anna Borkowska https://szpitaldzieciecybialystok.sisco.info/?id=416
187 mgr Piotr Kołos https://uskwb.pl/dyrekcja-szpitala/
188 mgr Sylwia Ignaczak-Piotrowska https://szpitaldzieciecybialystok.sisco.info/?id=416
189 mgr Wojciech Łuczaj Prawo i Sprawiedliwość https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3739309 kandydat na burmistrza Czyżewa 2024 z KW PiS
190 mgr inż. Sławomir Wilczewski https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/
191 mgr inż. Wojciech Roszkowski https://szpitaldzieciecybialystok.sisco.info/?id=316
192 ppłk Małgorzata Błażewicz https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/
193 prof. dr hab. Dariusz Lebensztejn https://uskbialystok.sisco.info/wladze,36_8
194 prof. dr hab. Jerzy Pałka https://uskbialystok.sisco.info/wladze,36_8
195 prof. dr hab. Sławomir Terlikowski https://uskbialystok.sisco.info/wladze,36_8
196 prof. dr hab. Wojciech Miltyk https://uskbialystok.sisco.info/wladze,36_8
197 prof. dr hab. n. med. Andrzej Dąbrowski https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/
198 prof. dr hab. n. med. Anna Niemcunowicz-Janica https://szpitaldzieciecybialystok.sisco.info/?id=416
199 prof. dr hab. n. med. Anna Wasilewska https://szpitaldzieciecybialystok.sisco.info/?id=316
200 prof. dr hab. n. med. Beata Naumnik https://szpitaldzieciecybialystok.sisco.info/?id=416
201 prof. dr hab. n. med. Elżbieta Tryniszewska https://szpitaldzieciecybialystok.sisco.info/?id=416
202 prof. dr hab. n. med. Halina Car https://szpitaldzieciecybialystok.sisco.info/?id=416
203 prof. dr hab. n. med. Jan Kochanowicz https://uskwb.pl/dyrekcja-szpitala/
204 prof. dr hab. n. med. Jarosław Daniluk https://szpitaldzieciecybialystok.sisco.info/?id=416
205 płk SG Marek Sochański https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/
206 st. bryg. Marcin Wierel https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/
+397
View File
@@ -0,0 +1,397 @@
# Szpitale województwa podlaskiego — kadra kierownicza, organy nadzorcze i afiliacje polityczne
> **Materiał roboczy do celów dziennikarskich.** Stan na dzień dostępu do źródeł: **2026-07-06**.
> Struktura każdego wpisu: **szpital → funkcja → imię i nazwisko → afiliacja partyjna → źródło**.
---
## Nota metodologiczna (przeczytaj przed użyciem)
**Rozróżnienie organów.** Zdecydowana większość szpitali w regionie to **SPZOZ** (samodzielne publiczne zakłady opieki zdrowotnej). Ich organem opiniodawczo-nadzorczym jest **RADA SPOŁECZNA** (art. 48 ustawy o działalności leczniczej) — **to nie jest „rada nadzorcza"**. Rada nadzorcza występuje tylko w szpitalach prowadzonych jako **spółki prawa handlowego** (w tym zestawieniu: **Szpital Powiatowy w Zambrowie sp. z o.o.**).
**Jak czytać kolumnę „afiliacja".**
- **Nazwa partii/komitetu** = osoba została wybrana z listy tego komitetu w wyborach samorządowych 2024 lub jest z nim jawnie związana, z podaniem źródła.
- **„brak/niepotwierdzona"** = nie ustalono afiliacji (typowo: menedżerowie szpitali, urzędnicy, przedstawiciele izb zawodowych i służb mundurowych).
- **„NIEZWERYFIKOWANE"** = dopasowanie oparte wyłącznie na zgodności imienia i nazwiska lub na źródle wtórnym/historycznym — **wymaga potwierdzenia przed publikacją**.
- **KWW / komitet lokalny ≠ partia.** Wiele osób startuje z lokalnych komitetów wyborców (np. „KWW Ziemia Hajnowska"); przynależność do ogólnopolskiej partii pozostaje wtedy nieustalona.
**Ograniczenia źródłowe.** Domeny BIP w formacie `*.wrotapodlasia.pl` są w trakcie migracji na `*.podlaskie.eu`; część starych adresów nie odpowiada. Afiliacje radnych sejmiku weryfikowano częściowo wobec Wikipedii (skład Sejmiku Woj. Podlaskiego VII kad.) — **przed publikacją zalecana kontrola krzyżowa w wyszukiwarce kandydatów PKW 2024** (`samorzad2024.pkw.gov.pl`). Imienne składy kilku rad społecznych nie są publikowane online (oznaczone „brak danych") — wymagają wniosku o informację publiczną.
**Kluczowy wniosek analityczny.** Upolitycznienie organów nadzorczych koncentruje się w **radach społecznych szpitali WOJEWÓDZKICH** (Śniadeckiego, Białostockie Centrum Onkologii, szpitale w Łomży i Suwałkach) — tam zasiadają czynni radni sejmiku, marszałkowie, posłowie i senatorowie — oraz w **radach szpitali POWIATOWYCH**, gdzie z reguły zasiadają starosta (jako przewodniczący), radni powiatu i wójtowie/burmistrzowie gmin. Rady szpitali **uniwersyteckich** (USK, UDSK) i **MSWiA** są obsadzone instytucjonalnie (akademicy UMB, służby mundurowe, izby zawodowe) — bez potwierdzonych afiliacji partyjnych.
---
# CZĘŚĆ I — Szpitale wojewódzkie, uniwersyteckie i resortowe
## 1. Uniwersytecki Szpital Kliniczny w Białymstoku (USK)
*Forma prawna: SPZOZ. Organ tworzący: Uniwersytet Medyczny w Białymstoku (UMB). Organ nadzorczy: Rada Społeczna.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor Naczelny | prof. dr hab. n. med. Jan Kochanowicz | brak/niepotwierdzona | https://uskwb.pl/dyrekcja-szpitala/ |
| Z-ca ds. Lecznictwa | dr hab. n. med. Marzena Wojewódzka-Żelezniakowicz | brak/niepotwierdzona | https://uskwb.pl/dyrekcja-szpitala/ |
| Z-ca ds. Finansowych / Gł. Księgowy | mgr Beata Kropiewnicka | brak/niepotwierdzona | https://uskwb.pl/dyrekcja-szpitala/ |
| Z-ca ds. Techniczno-Inwestycyjnych | mgr Piotr Kołos | brak/niepotwierdzona | https://uskwb.pl/dyrekcja-szpitala/ |
| Rada Społeczna — Przewodniczący | prof. dr hab. Dariusz Lebensztejn (UMB) | brak/niepotwierdzona (środ. akademickie) | https://uskbialystok.sisco.info/wladze,36_8 |
| Rada — Z-ca Przewodniczącego | prof. dr hab. Wojciech Miltyk (UMB) | brak/niepotwierdzona | https://uskbialystok.sisco.info/wladze,36_8 |
| Rada — Sekretarz | dr hab. Joanna Reszeć-Giełażyn (UMB) | brak/niepotwierdzona | https://uskbialystok.sisco.info/wladze,36_8 |
| Rada — członek (Min. Zdrowia) | Bartosz Niebrzydowski | brak/niepotwierdzona | https://uskbialystok.sisco.info/wladze,36_8 |
| Rada — członek (Wojewoda Podlaski) | Marcin Borsuk | brak/niepotwierdzona | https://uskbialystok.sisco.info/wladze,36_8 |
| Rada — członek (Marszałek Woj.) | Marek Malinowski | Wicemarszałek woj.; w Sejmiku VII kad. radny **niezrzeszony** (historycznie PSL) — do potwierdzenia | https://uskbialystok.sisco.info/wladze,36_8 |
| Rada — członek (Okr. Izba Lekarska) | Henryk Grzesiak | brak/niepotwierdzona | https://uskbialystok.sisco.info/wladze,36_8 |
| Rada — członek (Izba Piel. i Poł.) | mgr Elżbieta Sienkiewicz | brak/niepotwierdzona | https://uskbialystok.sisco.info/wladze,36_8 |
| Rada — członek (UMB) | prof. dr hab. Sławomir Terlikowski | brak/niepotwierdzona | https://uskbialystok.sisco.info/wladze,36_8 |
| Rada — członek (UMB) | prof. dr hab. Jerzy Pałka | brak/niepotwierdzona | https://uskbialystok.sisco.info/wladze,36_8 |
## 2. Uniwersytecki Dziecięcy Szpital Kliniczny im. L. Zamenhofa (UDSK), Białystok
*Forma prawna: SPZOZ. Organ tworzący: UMB. Organ nadzorczy: Rada Społeczna.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | prof. dr hab. n. med. Anna Wasilewska | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=316 |
| Z-ca ds. Ekonomicznych | dr n. o zdr. Cezary Rzemek | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=316 |
| Z-ca ds. Administr.-Techn. | mgr inż. Wojciech Roszkowski | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=316 |
| Z-ca ds. Pielęgniarstwa | mgr Elżbieta Sienkiewicz | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=316 |
| Rada Społeczna — Przewodnicząca | prof. dr hab. n. med. Beata Naumnik | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=416 |
| Rada — członek | mgr Sylwia Ignaczak-Piotrowska | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=416 |
| Rada — członek | mgr Katarzyna Bardłowska | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=416 |
| Rada — członek | mgr Monika Anna Borkowska | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=416 |
| Rada — członek | dr n. med. Ryta Filipkowska | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=416 |
| Rada — członek | mgr Bożena Kostro | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=416 |
| Rada — członek | prof. dr hab. n. med. Anna Niemcunowicz-Janica | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=416 |
| Rada — członek | prof. dr hab. n. med. Elżbieta Tryniszewska | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=416 |
| Rada — członek | prof. dr hab. n. med. Halina Car | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=416 |
| Rada — członek | prof. dr hab. n. med. Jarosław Daniluk | brak/niepotwierdzona | https://szpitaldzieciecybialystok.sisco.info/?id=416 |
## 3. SP ZOZ Wojewódzki Szpital Zespolony im. J. Śniadeckiego w Białymstoku
*Forma prawna: SPZOZ. Organ tworzący: Województwo Podlaskie (Samorząd Województwa). Organ nadzorczy: Rada Społeczna.*
*Dyrektor: **Bogdan Kalicki** — p.o. od 3.03.2025 (po odwołaniu Cezarego Nowosielskiego), oficjalne powołanie w sierpniu 2025. Wcześniej dyr. Urszula Łapińska (odwołana 2023).*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | Bogdan Kalicki | brak/niepotwierdzona | https://podlaskie.eu/zdrowie/bogdan-kalicki-oficjalnie-objal-stanowisko-dyrektora-sniadecji.html |
| Rada Społeczna — Przewodniczący | Karol Pilecki | **Koalicja Obywatelska** (radny Sejmiku VII kad.); *jedno źródło prasowe: PO; historycznie wiązany z PSL — do odnotowania* | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek (przedst. Wojewody) | Joanna Sosnowska | brak danych | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Bartosz Czaczkowski | brak danych | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Bogusław Dębski | **Prawica Rzeczypospolitej** (radny Sejmiku; historycznie PSL) | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Jarosław Zygmunt Dworzański | **Koalicja Obywatelska** (radny Sejmiku; b. marszałek woj.) | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Henryk Gryko | brak danych | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Paweł Jamróz | brak danych | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Jan Maksymilian Janiszewski | brak danych | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Katarzyna Kluczyk-Ceckowska | brak danych | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Rafał Kołakowski | brak danych | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Wojciech Łuczaj | brak danych (możliwie Łuczaj-Pogorzelski, PSL — **NIEZWERYFIKOWANE**) | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Tomasz Stefan Madras | **Prawo i Sprawiedliwość** (radny Sejmiku VII kad.; b. wicewojewoda) | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Jarosław Matwiejuk | historycznie SLD/Lewica (b. poseł SLD) — **NIEZWERYFIKOWANE** | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Andrzej Parafiniuk | brak danych | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek | Lech Rutkowski | brak danych | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek (przedst. Miasta Białystok) | Wojciech Strzałkowski | brak danych (**NIEZWERYFIKOWANE**) | https://bip-spzozwszjs.podlaskie.eu/rada.html |
| Rada — członek (przedst. Rektora UMB) | Irina Kowalska | brak/niepotwierdzona | https://bip-spzozwszjs.podlaskie.eu/rada.html |
## 4. Białostockie Centrum Onkologii im. M. Skłodowskiej-Curie, Białystok
*Forma prawna: SPZOZ. Organ tworzący: Województwo Podlaskie. Rada Społeczna powołana Uchwałą Nr 72/1442/2025 Zarządu Woj. Podlaskiego z 11.03.2025.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | dr n. o zdr. Magdalena Joanna Borkowska | brak/niepotwierdzona | https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html |
| Z-ca ds. Lecznictwa | dr n. med. Dorota Elżbieta Kazberuk | brak/niepotwierdzona | https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html |
| Z-ca ds. Koordynacji Onkologicznej | dr n. o zdr. Elżbieta Potentas | brak/niepotwierdzona | https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html |
| Rada Społeczna — Przewodnicząca | Anna Augustyn | **Koalicja Obywatelska** (radna Sejmiku VII kad.) | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Cezary Cieślukowski | **PSL** (Przewodniczący Sejmiku VII kad.) | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Jarosław Zygmunt Dworzański | **Koalicja Obywatelska** (radny Sejmiku) | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Bogdan Dyjuk | **PSL** (radny Sejmiku; członek Zarządu Woj.) | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Janusz Dzięcioł | **NIEZWERYFIKOWANE** (inna osoba niż zmarły poseł PO; afiliacji nie potwierdzono) | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Ewa Feszler | brak danych | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Robert Jabłoński | prawdopodobnie **PiS** (radny Sejmiku o tym nazwisku) — **NIEZWERYFIKOWANE** co do tożsamości | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Leszek Maciej Lulewicz | brak danych | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Jacek Piorunek | **Koalicja Obywatelska** (radny Sejmiku) | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Agnieszka Zaręba | brak danych | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Agnieszka Zawistowska | brak danych | https://www.onkologia.bialystok.pl/pages/rada-spoleczna |
| Rada — członek | Maciej Żywno | **Polska 2050** (senator, wicemarszałek Senatu) | https://www.senat.gov.pl/sklad/senatorowie/senator,1072,11,maciej-zywno.html |
## 5. SP ZOZ MSWiA w Białymstoku im. M. Zyndrama-Kościałkowskiego
*Forma prawna: SPZOZ. Organ tworzący: Minister Spraw Wewnętrznych i Administracji. Organ nadzorczy: Rada Społeczna (obsada resortowo-instytucjonalna).*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | Marek Stanisław Karp | brak/niepotwierdzona (p.o. od 20.02.2024, nast. dyrektor) | https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna |
| Rada Społeczna — Przewodniczący (MSWiA) | Daniel Szutko | brak/niepotwierdzona | https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/ |
| Rada — Z-ca Przewodniczącego (Policja) | mgr inż. Sławomir Wilczewski | brak/niepotwierdzona | https://bip.zozmswia.bialystok.pl/.../rada-spoleczna/ |
| Rada — członek (PSP) | st. bryg. Marcin Wierel | brak/niepotwierdzona | https://bip.zozmswia.bialystok.pl/.../rada-spoleczna/ |
| Rada — członek (Straż Graniczna) | płk SG Marek Sochański | brak/niepotwierdzona | https://bip.zozmswia.bialystok.pl/.../rada-spoleczna/ |
| Rada — członek (Służba Więzienna) | ppłk Małgorzata Błażewicz | brak/niepotwierdzona | https://bip.zozmswia.bialystok.pl/.../rada-spoleczna/ |
| Rada — członek (Nacz. Rada Lekarska) | dr n. med. Anatol Aksiucik | brak/niepotwierdzona | https://bip.zozmswia.bialystok.pl/.../rada-spoleczna/ |
| Rada — członek (NRPiP) | dr n. med. Beata Olejnik | brak/niepotwierdzona | https://bip.zozmswia.bialystok.pl/.../rada-spoleczna/ |
| Rada — członek (UMB) | prof. dr hab. n. med. Andrzej Dąbrowski | brak/niepotwierdzona | https://bip.zozmswia.bialystok.pl/.../rada-spoleczna/ |
| Rada — członek (MSWiA) | Krzysztof Turowski | brak/niepotwierdzona | https://bip.zozmswia.bialystok.pl/.../rada-spoleczna/ |
## 6. SP Psychiatryczny ZOZ im. dr. St. Deresza w Choroszczy
*Forma prawna: SP Psychiatryczny ZOZ. Organ tworzący: Województwo Podlaskie. Organ nadzorczy: Rada Społeczna.*
*Dyrektor: obecnie **p.o. Alicja Skindzielewska**. W konkursie (VVI 2026) wyłoniono **Andrzeja Szewczuka** (dotąd dyr. SP ZOZ Siemiatycze), obejmie stanowisko od VIII.2026 — na 2026-07-06 formalnie jeszcze nie objął.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| p.o. Dyrektora | Alicja Skindzielewska | brak/niepotwierdzona | https://bip-sppzozc.podlaskie.eu/ |
| Dyrektor-elekt (od VIII.2026) | Andrzej Szewczuk | brak/niepotwierdzona | https://poranny.pl/... (konkurs Choroszcz 2026) |
| Z-ca ds. Lecznictwa | lek. Justyna Anna Piłasiewicz | brak/niepotwierdzona | https://bip-sppzozc.podlaskie.eu/ |
| Gł. Księgowa | Ewelina Cecotka-Białokozowicz | brak/niepotwierdzona | https://bip-sppzozc.podlaskie.eu/ |
| Naczelna Pielęgniarka | dr n. med. Nadzieja Sołowiej | brak/niepotwierdzona | https://bip-sppzozc.podlaskie.eu/ |
| Rada Społeczna — Przewodnicząca | Monika Miklaszewicz | brak danych | https://bip-sppzozc.podlaskie.eu/ |
| Rada Społeczna — pozostali członkowie | **brak danych** (skład nieopublikowany na BIP) | — | https://bip-sppzozc.podlaskie.eu/ |
## 7. Szpital Wojewódzki im. Kardynała Stefana Wyszyńskiego w Łomży
*Forma prawna: SPZOZ. Organ tworzący: Samorząd Województwa Podlaskiego. Rada Społeczna — Uchwała Zarządu Woj. Podlaskiego nr 44/866/2024 z 12.11.2024.*
*Dyrektor: **Dariusz Domasiewicz** (od 11.03.2025, konkurs, kadencja 6-letnia); wcześniej sam zasiadał w tej Radzie Społecznej — ustąpił obejmując dyrekcję.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | Dariusz Domasiewicz | Radny Rady Miejskiej Łomży, KWW „Przyjazna Łomża"; kandydat na prezydenta Łomży 2024 | https://www.rynekzdrowia.pl/.../Nowy-dyrektor-w-Szpitalu-Wojewodzkim-w-Lomzy,269250,1.html |
| Rada Społeczna — Przewodnicząca | Maria Konopka | kandydatka **Koalicji Obywatelskiej** do sejmiku 2024 (bez mandatu); prezes Tow. Ziemi Łomżyńskiej | https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna |
| Rada — członek | Marek Olbryś | **PiS** — wicemarszałek woj. / radny Sejmiku | https://www.portalsamorzadowy.pl/osoba/marek-olbrys,876.html |
| Rada — członek | Jacek Piorunek | **Koalicja Obywatelska** — radny Sejmiku | https://www.portalsamorzadowy.pl/osoba/jacek-piorunek,6388.html |
| Rada — członek | Bernadeta Krynicka | **PiS** — radna Sejmiku (2024); była posłanka PiS | https://pl.wikipedia.org/wiki/Bernadeta_Krynicka |
| Rada — członek | Lech Marek Szabłowski | **PiS** — starosta łomżyński / radny powiatu | https://www.portalsamorzadowy.pl/osoba/lech-szablowski,55269.html |
| Rada — członek | Krzysztof Ryszard Kozicki | **PiS** — wójt Piątnicy 2024 (wybrany z KW PiS); wcześniej działacz PSL | https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3469307 |
| Rada — członek | Andrzej Borusiewicz | brak potwierdzonej afiliacji; prorektor MANS w Łomży, wiceprezes ARiMR | https://pl.wikipedia.org/wiki/Andrzej_Borusiewicz |
| Rada — członek | Karol Kossakowski | niepotwierdzona (kandydował do sejmiku, rej. Zambrów; komitet niepotwierdzony) | https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna |
| Rada — członek | Stanisław Kossakowski | brak danych / niepotwierdzona | https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna |
| Rada — członek | Andrzej Szymański | brak danych / niepotwierdzona | https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna |
| Rada — członek | Anna Malwina Szynkiewicz | brak danych / niepotwierdzona | https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna |
| Rada — członek | Dorota Waszkiewicz | brak danych / niepotwierdzona | https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna |
| Rada — członek | Renata Szydłowska | brak danych / niepotwierdzona | https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna |
| Rada — członek | Milena Siok | brak danych (możliwie przedst. Wojewody — **NIEZWERYFIKOWANE**) | https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna |
| Rada — członek | Marcin Biedrzycki | brak danych / niepotwierdzona | https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna |
| Rada — członek | Janusz Józef Cieciórski | brak danych / niepotwierdzona | https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna |
## 8. Szpital Wojewódzki im. dr. Ludwika Rydygiera w Suwałkach
*Forma prawna: SPZOZ. Organ tworzący: Województwo Podlaskie. Organ nadzorczy: Rada Społeczna.*
*Rada Społeczna: uchwała Sejmiku Woj. Podlaskiego nr LIII/845/2023, zmieniona X/136/2025 / druk nr 191 z XI sesji Sejmiku (31.03.2025). Afiliacje 12 członków — częściowo z dopasowań po nazwisku, patrz oznaczenia.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | Adam Szałanda | brak/niepotwierdzona (dyr. od 2011; kariera w UW i pogotowiu) | https://bip-spsws.podlaskie.eu/6b54de5acb7.html |
| Z-ca ds. Lecznictwa | lek. Włodzimierz Żaworonok | brak danych | https://bip-spsws.podlaskie.eu/6b54de5acb7.html |
| Gł. Księgowy | Agnieszka Kapczyńska | brak danych | https://bip-spsws.podlaskie.eu/6b54de5acb7.html |
| Rada Społeczna — Przewodnicząca | Anna Naszkiewicz | **Koalicja Obywatelska** — radna Sejmiku (2014/2018/2024), b. wicemarszałek, wiceprzew. Sejmiku | https://samorzad.podlaskie.eu/radni/naszkiewicz-anna.html |
| Rada — członek | Bogdan Dyjuk | **Trzecia Droga (PSLPolska 2050)** — radny Sejmiku, czł. Zarządu Woj. | https://www.portalsamorzadowy.pl/osoba/bogdan-dyjuk,2177.html |
| Rada — członek | Tadeusz Buchowiecki | brak/niepotwierdzona (ślad „komitet poparcia" — **NIEZWERYFIKOWANE**) | druk nr 191 Sejmiku (bip.podlaskie.eu) |
| Rada — członek | Wojciech Wasilewski | brak/niepotwierdzona (**NIEZWERYFIKOWANE**, zbieżność nazwisk) | druk nr 191 Sejmiku |
| Rada — członek | Marta Żybura | kandydatka KO do Rady Miejskiej 2024 (bez mandatu) — **NIEZWERYFIKOWANE** | https://www.suwalki24.pl/article/1,radni-i-kandydaci... |
| Rada — członek | Dariusz Bogdan | b. wiceminister gospodarki (rząd POPSL); przynależność partyjna niepotwierdzona | https://pl.wikipedia.org/wiki/Dariusz_Bogdan |
| Rada — członek | Joanna Jędzul | KWW „Razem dla Suwałk Cz. Renkiewicz" (komitet lokalny) | https://suwalki.naszemiasto.pl/... |
| Rada — członek | Dorota Iwanowicz | Trzecia Droga (radna gm. Suwałki) — **NIEZWERYFIKOWANE** (zbieżność nazwisk) | https://www.farmer.pl/wybory/osoba/dorota-iwanowicz,221972.html |
| Rada — członek | Daniel Leszek Makarewicz | KWW Suwalskie Porozumienie Prawicy — **NIEZWERYFIKOWANE** | https://www.suwalki24.pl/article/57,kandydaci-na-radnych-w-suwalkach... |
| Rada — członek | Elżbieta Luto | brak/niepotwierdzona | druk nr 191 Sejmiku |
| Rada — członek | Justyna Drażba | brak/niepotwierdzona | druk nr 191 Sejmiku |
| Rada — członek | Grzegorz Paweł Matys | brak/niepotwierdzona | druk nr 191 Sejmiku |
| Rada — członek | Krystian Małyszko | brak/niepotwierdzona | druk nr 191 Sejmiku |
| Rada — członek (przedst. Wojewody) | brak danych | — | nazwiska nie ustalono |
## 9. Specjalistyczny Psychiatryczny SP ZOZ w Suwałkach
*Forma prawna: SPZOZ. Organ tworzący: Województwo Podlaskie. Rada Społeczna — ost. zmiana Uchwałą nr 81/1632/2025 Zarządu Woj. Podlaskiego z 16.04.2025.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | Bożena Łapińska | brak/niepotwierdzona | https://bip-spspzozs.podlaskie.eu/ |
| Z-ca ds. Lecznictwa | lek. Michał Naczas | brak danych | https://bip-spspzozs.podlaskie.eu/ |
| Gł. Księgowy | Marek Wasilewski | brak danych | https://bip-spspzozs.podlaskie.eu/ |
| Rada Społeczna — Przewodniczący | Władysław Renowicki | brak/niepotwierdzona | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek (przedst. Wojewody) | Eliza Kupich | brak danych | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Waldemar Kwaterski | **Koalicja Obywatelska** (radny Sejmiku; dyr. SP ZOZ Sejny) — **NIEZWERYFIKOWANE** co do tożsamości na tej liście | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Jacek Niedźwiedzki | możliwie KO/PO (poseł z Suwałk) — **NIEZWERYFIKOWANE (zbieżność nazwisk)** | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Elżbieta Baranowska | brak danych | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Natalia Bogdan-Czarnowska | brak danych | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Anna Maria Gawlińska | brak danych | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Anna Kumiszcza | brak danych | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Tomasz Romualdowski | brak danych | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Bożena Jadwiga Chmielewska | brak danych | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Dorota Iwanowicz | brak danych | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Sławomir Jan Szeszko | brak danych | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
| Rada — członek | Aneta Jolanta Szmidt | brak danych | https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html |
---
# CZĘŚĆ II — Szpitale powiatowe
## 10. SP ZOZ w Augustowie
*Forma prawna: SPZOZ. Organ tworzący: Powiat Augustowski. Rada Społeczna — uchwała bazowa 270/XXXIV/2022 (29.12.2022) znowelizowana uchwałą 14/II/2024 (6.06.2024).*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor (od 1.09.2024) | Adam Dębski | brak/niepotwierdzona (b. rzecznik Podlaskiego OW NFZ) | https://bip-spzoz-staugustow.podlaskie.eu/organy_wadze/kierownictwo_szpitala/ |
| Z-ca ds. Lecznictwa | Wioletta Tomaszycka-Bednarczyk | brak danych | https://bip-spzoz-staugustow.podlaskie.eu/organy_wadze/kierownictwo_szpitala/ |
| Rada Społeczna — Przewodniczący (Starosta) | Piotr Rusiecki | **PiS** — starosta augustowski, radny powiatu | https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1 |
| Rada — członek | Marek Dobkowski | KWW Samorząd Powiatu Augustowskiego | https://www.dziennikpowiatowy.pl/artykul/13664,wyniki-wyborow-do-rady-powiatu-w-augustowie |
| Rada — członek | Waldemar Jedliński | **PiS**; członek Zarządu Powiatu | https://www.dziennikpowiatowy.pl/artykul/13664 |
| Rada — członek | Elżbieta Puczyłowska | **PiS**; wiceprzewodnicząca Rady Powiatu | https://www.dziennikpowiatowy.pl/artykul/13664 |
| Rada — członek | Alicja Dobrowolska | **PiS**; członek Zarządu Powiatu | https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1 |
| Rada — członek | Dariusz Jan Szkiłądź | KWW BAU 2024 | https://www.dziennikpowiatowy.pl/artykul/13664 |
| Rada — członek (przedst. Wojewody) | Jadwiga Józefa Milewska | urzędniczka Podlaskiego UW — **NIEZWERYFIKOWANE (zbieżność nazwisk)** | https://pl.wikipedia.org/wiki/Jadwiga_Milewska |
## 11. SP ZOZ w Bielsku Podlaskim
*Forma prawna: SPZOZ. Organ tworzący: Powiat Bielski. Rada Społeczna powołana na II sesji Rady Powiatu (16.05.2024, pierwsze posiedzenie 23.05.2024).*
*Dyrektor: **Piotr Selwesiuk** — na 2026-07-06 formalnie dyrektor, ale 29.06.2026 złożył wypowiedzenie (2-mies. okres). Obowiązki miałby przejąć z-ca ds. lecznictwa.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | Piotr Selwesiuk | niepotwierdzona (kandydat 2024 z KW Koalicja Bielska, bez mandatu) | https://www.spzoz-bielsk.pl/o-nas/dyrekcja/ |
| Z-ca ds. Lecznictwa | dr n. med. Arsalan Azzaddin | brak/niepotwierdzona | https://www.spzoz-bielsk.pl/o-nas/dyrekcja/ |
| Rada Społeczna — Przewodniczący | Piotr Bożko | KW Koalicja Bielska; wicestarosta, radny powiatu | https://samorzad.gov.pl/web/powiat-bielski/ii-sesja-rady-powiatu-w-bielsku-podlaskim |
| Rada — członek | Elżbieta Fionik | KW Koalicja Bielska; radna powiatu | https://www.portalsamorzadowy.pl/powiat/bielski,295.html |
| Rada — członek | Maria Ryżyk | KW Koalicja Bielska; radna powiatu | https://www.portalsamorzadowy.pl/powiat/bielski,295.html |
| Rada — członek | Andrzej Leszczyński | KWW „Nasze Podlasie"; Przew. Rady Powiatu | https://www.portalsamorzadowy.pl/powiat/bielski,295.html |
| Rada — członek | Adam Łęczycki | KWW „Nasze Podlasie"; radny powiatu | https://www.portalsamorzadowy.pl/powiat/bielski,295.html |
| Rada — członek | Zbigniew Jan Szpakowski | KWW „Nasze Podlasie"; radny powiatu, czł. Zarządu | https://www.portalsamorzadowy.pl/powiat/bielski,295.html |
| Rada — członek | Jan Niewiński | **PiS**; radny powiatu | https://www.portalsamorzadowy.pl/osoba/jan-niewinski,48951.html |
| Rada — członek (przedst. Wojewody) | Walentyna Tyszko-Dubow | działaczka PO/KO z Hajnówki — **NIEZWERYFIKOWANE** (poza listą radnych) | https://podlaskie.eu/spoleczenstwo/marszalek-powolal-podlaska-rade-kobiet.html |
## 12. SP ZOZ w Hajnówce
*Forma prawna: SPZOZ. Organ tworzący: Powiat Hajnowski. Rada Społeczna — Uchwała Nr III/13/2024 Rady Powiatu Hajnowskiego z 28.05.2024.*
*Uwaga: dwa niezależne raporty podały rozbieżne komitety dla przewodniczącego i przedstawiciela wojewody — patrz „Rozbieżności".*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | Grzegorz Tomaszuk | brak/niepotwierdzona | https://spzozhajnowka.pl/strona-3343-dyrekcja.html |
| Z-ca ds. Lecznictwa | Tomasz Musiuk | brak/niepotwierdzona | https://spzozhajnowka.pl/strona-3343-dyrekcja.html |
| Rada Społeczna — Przewodniczący (Starosta) | Andrzej Skiepko | komitet lokalny (KWW Ziemia Hajnowska **lub** KWW Porozumienie Samorządowe Regionu Puszczy Białowieskiej — **źródła rozbieżne**) | https://www.portalsamorzadowy.pl/osoba/andrzej-skiepko,17764.html |
| Rada — członek (przedst. Wojewody) | Jerzy Sirak | **rozbieżne**: KO (jeden raport) vs historycznie SLD/komitet lokalny (drugi) — b. burmistrz Hajnówki, radny powiatu | https://www.portalsamorzadowy.pl/osoba/jerzy-sirak,1302.html |
| Rada — członek (Wicestarosta) | Justyna Jarmocik | **Trzecia Droga (PSL / Polska 2050)** | https://www.portalsamorzadowy.pl/powiat/hajnowski,350.html |
| Rada — członek (Gmina Białowieża) | Albert Waldemar Litwinowicz | wójt Białowieży; komitet niepotwierdzony | Uchwała III/13/2024 |
| Rada — członek (Gmina Czyże) | Jerzy Wasiluk | wójt Czyż; możliwie Trzecia Droga — do potwierdzenia | Uchwała III/13/2024 |
| Rada — członek (Gmina Dubicze Cerkiewne) | Leon Małaszewski | wójt; KWW Ziemia Hajnowska (niepotwierdzone partyjnie) | Uchwała III/13/2024 |
| Rada — członek (Gmina Hajnówka) | Lucyna Smoktunowicz | wójt; KWW „Nasza Przyszłość", bezpartyjna | https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3412858 |
| Rada — członek (Miasto Hajnówka) | Ireneusz Roman Kiendyś | burmistrz; KWW Hajnówka Razem, bezpartyjny | https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3459046 |
| Rada — członek (Kleszczele) | Mirosław Markiewicz | burmistrz; KWW Roztropna Troska o Dobro Gminy | https://w.radio.bialystok.pl/wiadomosci/index/id/236847 |
| Rada — członek (Gmina Narew) | Aneta Leonowicz | wójt; KWW „Narew Odnowa" | https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3588266 |
| Rada — członek (Gmina Narewka) | Jarosław Gołubowski | wójt; KWW Wspólna Narewka | https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3421407 |
## 13. SP ZOZ w Siemiatyczach (Szpital Powiatowy)
*Forma prawna: SPZOZ. Organ tworzący: Powiat Siemiatycki. Rada Społeczna — skład po zmianach z czerwca 2024.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | Andrzej Szewczuk | brak/niepotwierdzona *(w VI 2026 wygrał konkurs na dyr. szpitala w Choroszczy — przejście od VIII.2026)* | https://spzozsiemiatycze.pl/?page_id=395 |
| Z-ca ds. Lecznictwa | Lech Zenon Hackiewicz | brak/niepotwierdzona | https://spzozsiemiatycze.pl/?page_id=395 |
| Rada Społeczna — Przewodniczący (Starosta) | Mariusz Cieślik | **PSL** (wybrany z KWW Ruch Ludowy) | https://www.radio.bialystok.pl/wiadomosci/index/id/238459 |
| Rada — członek (Zarząd Powiatu) | Sylwester Zgierun | **PSL** (KWW Ruch Ludowy) | https://siemiatycze.podlasie24.pl/region/zarzad-powiatu-siemiatyckiego-z-absolutorium-za-rok-2023-20240615102143 |
| Rada — członek (przedst. Wojewody, Przew. Rady Powiatu) | Mariusz Pyzowski | **Koalicja Obywatelska** | https://www.portalsamorzadowy.pl/osoba/mariusz-pyzowski,34169.html |
| Rada — członek | Jan Żeruń | KWW SPW (Siemiatyckie Porozumienie Wyborcze) | https://siemiatycze.podlasie24.pl/region/... |
| Rada — członek (Wójt Gminy Siemiatycze) | Edward Krasowski | własny KWW; afiliacja partyjna niepotwierdzona | https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3739772 |
| Rada — członek (Burmistrz Siemiatycz) | Piotr Siniakowicz | **PO/KO** (startował z KWW Dla Siemiatycz Razem) — jeden raport: niepotwierdzona | https://pl.wikipedia.org/wiki/Piotr_Siniakowicz |
## 14. SP ZOZ w Sokółce
*Forma prawna: SPZOZ. Organ tworzący: Powiat Sokólski. Rada Społeczna (wg statutu 11 osób) — pełnego imiennego składu 2024+ nie ustalono z jawnych źródeł.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | Anna Marta Aniśkiewicz | niepotwierdzona (kandydatka 2024 z KWW „Razem dla Sokółki"; b. urzędniczka starostwa) | https://www.radio.bialystok.pl/wiadomosci/index/id/228040 |
| Z-ca ds. Lecznictwa | Adam Pietruczuk | brak/niepotwierdzona | https://szpitalsokolka.pl/dyrekcja/ |
| Z-ca ds. Ekonomicznych | Maria Julita Budrowska | brak/niepotwierdzona | https://szpitalsokolka.pl/dyrekcja/ |
| Rada Społeczna — Przewodniczący (Starosta) | Piotr Rećko | **PiS** — starosta sokólski, radny powiatu | https://samorzad2024.pkw.gov.pl/samorzad2024/pl/rada_powiatu/kandydat/3296821 |
| Rada — członek | Cezary Możejko | możliwie wójt gm. Sidra — **NIEZWERYFIKOWANE (zbieżność nazwisk)** | https://ssl.esesja.pl/zalaczniki/298157/... |
| Rada — członek | Krzysztof Pawłowski | **PiS** (radny powiatu, wiceprzew. Rady Powiatu) — tożsamość wysoce prawdopodobna | https://www.radio.bialystok.pl/wiadomosci/index/id/237569 |
| Rada — pozostali (7 z 9 przedst. Rady Powiatu + przedst. Wojewody) | **brak danych** | — | uchwała powołująca nieodnaleziona online |
## 15. SP ZOZ w Mońkach
*Forma prawna: SPZOZ. Organ tworzący: Powiat Moniecki. Rada Społeczna — imiennego składu 2024+ nie opublikowano online (uchwała prawdopodobnie z VIII sesji Rady Powiatu, 18.12.2024).*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | lek. med. Sebastian Wysocki | brak/niepotwierdzona | https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/ |
| Z-ca ds. Lecznictwa (p.o.) | lek. med. Łucja Chojnowska | brak/niepotwierdzona | https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/ |
| Z-ca ds. Pielęgniarstwa | mgr Halina Skibicka | brak/niepotwierdzona | https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/ |
| Gł. Księgowa (p.o.) | Barbara Lubecka | brak/niepotwierdzona | https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/ |
| Rada Społeczna — Przewodnicząca (Starosta / osoba wyznaczona) | Joanna Kitlas | KWW BMN Aktywny Samorząd (starosta) — **NIEZWERYFIKOWANE**, czy przewodniczy osobiście | https://www.portalsamorzadowy.pl/powiat/moniecki,362.html |
| Rada Społeczna — pozostali członkowie | **brak danych** | — | uchwała niedostępna online |
## 16. Szpital Ogólny im. dr. Witolda Ginela w Grajewie
*Forma prawna: SPZOZ. Organ tworzący: Powiat Grajewski. Rada Społeczna — Uchwała Zarządu Powiatu Grajewskiego nr 83/379/25 z 1.12.2025 (skład wymaga potwierdzenia na oryginale — patrz zastrzeżenia).*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | mgr Marta Romanowska | brak/niepotwierdzona | https://www.szpital-grajewo.pl/dyrekcja |
| Z-ca ds. Lecznictwa (p.o.) | lek. med. Adam Leszczyński | brak/niepotwierdzona | https://www.szpital-grajewo.pl/dyrekcja |
| Gł. Księgowa | mgr Małgorzata Zdanewicz | brak/niepotwierdzona | https://www.szpital-grajewo.pl/dyrekcja |
| Naczelna Pielęgniarka | mgr Beata Szyszkowska | brak/niepotwierdzona | https://www.szpital-grajewo.pl/dyrekcja |
| Rada Społeczna — Przewodniczący (Starosta) | Waldemar Remfeld | **PiS** — starosta grajewski, radny powiatu | https://www.portalsamorzadowy.pl/powiat/grajewski,329.html |
| Rada — członek (przedst. Wojewody) | Milena Formejster | brak danych | agregacja cytująca uchwałę 83/379/25 |
| Rada — członek | Barbara Ciszewska | b. radna powiatu; Stow. „Nasza Przyszłość Grajewo"; komitet 2024 niepotwierdzony | https://e-grajewo.pl/wiadomosc,barbara-ciszewska--kandydatka-na-burmistrza-grajewa,54754.html |
| Rada — członek (Burmistrz Grajewa) | Maciej Paweł Bednarko | KWW Macieja Pawła Bednarko | https://www.grajewo.pl/pl/aktualnosci/maciej-pawel-bednarko-zaprzysiezony... |
| Rada — członek | Marek Kostrzewski | KWW Dobry Powiat (radny powiatu) — dopasowanie po nazwisku | https://bip.starostwograjewo.pl/612-kadencja-vii-2024-2029.html |
| Rada — członek | Jerzy Jan Nikliński | brak danych / **NIEZWERYFIKOWANE** | brak danych |
| Rada — członek | Bolesław Ruszczyk | **PiS** (radny powiatu) — dopasowanie po nazwisku | https://bip.starostwograjewo.pl/612-kadencja-vii-2024-2029.html |
## 17. Szpital Ogólny w Kolnie
*Forma prawna: SPZOZ. Organ tworzący: Powiat Kolneński. Rada Społeczna kadencji 2024-2028 (skład wg kolniak24.eu; uchwała Rady Powiatu z sesji 29.08.2024 — do potwierdzenia w BIP).*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | mgr Aneta Długozima | brak/niepotwierdzona | https://www.szpitalkolno.pl/struktura/dyrekcja/ |
| p.o. Z-ca ds. Lecznictwa | lek. Oksana Dowlaszewicz | brak/niepotwierdzona | https://www.szpitalkolno.pl/struktura/dyrekcja/ |
| p.o. Z-ca ds. Ekon.-Fin. / Gł. Księgowy | mgr Izabela Górska | brak/niepotwierdzona | https://www.szpitalkolno.pl/struktura/dyrekcja/ |
| Przełożona Pielęgniarek | mgr Alicja Urbańska | brak/niepotwierdzona | https://www.szpitalkolno.pl/struktura/dyrekcja/ |
| Rada Społeczna — Przewodniczący (Starosta) | Tadeusz Klama | KWW Porozumienie Samorządowe Powiatu Kolno; radny powiatu | https://www.portalsamorzadowy.pl/osoba/tadeusz-klama,58658.html |
| Rada — Wiceprzewodniczący | Michał Kulczewski | KWW Porozumienie Samorządowe Powiatu Kolno; radny powiatu | https://www.portalsamorzadowy.pl/osoba/michal-kulczewski,57069.html |
| Rada — członek (przedst. Wojewody) | Marek Samul | **PO****NIEZWERYFIKOWANE** co do stanu bieżącego (b. starosta kolneński, kandydat PO 2015) | https://kolniak24.eu/.../2899_marek-samul-wybiera-sie-do-sejmu-rp.html |
| Rada — członek (Gmina Grabowo) | Anita Krasińska | brak/niepotwierdzona (kier. OPS Grabowo) | https://kolniak24.eu/.../37295... |
| Rada — członek (Gmina Mały Płock) | Beata Helena Muniak | brak/niepotwierdzona (Sekretarz Gminy) | https://kolniak24.eu/.../37295... |
| Rada — członek (Gmina Kolno) | Robert Wacław Nadara | KWW Skuteczny i Przyjazny Samorząd; radny powiatu | https://www.radio.bialystok.pl/wybory-samorzadowe-2024/index/id/237569 |
| Rada — członek (Miasto Kolno) | Mariusz Rakowski | **PiS**; radny Rady Miasta Kolno | https://www.portalsamorzadowy.pl/osoba/mariusz-rakowski,106463.html |
## 18. Szpital Ogólny w Wysokiem Mazowieckiem
*Forma prawna: SPZOZ. Organ tworzący: Powiat Wysokomazowiecki. Imienny skład Rady Społecznej nie jest publikowany online.*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | mgr Wojciech Łuczaj | **PiS** — kandydat na burmistrza Czyżewa 2024 z KW PiS (II tura) | https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3739309 |
| Z-ca ds. Lecznictwa | lek. Julita Nikołajuk-Stasiuk | brak/niepotwierdzona | https://szpitalwysmaz.pl/pl/onas |
| Z-ca ds. Ekonomicznych | mgr Aleksandra Łapiak | brak/niepotwierdzona | https://szpitalwysmaz.pl/pl/onas |
| Przełożona Pielęgniarek | mgr Anna Wądołkowska | brak/niepotwierdzona | https://szpitalwysmaz.pl/pl/onas |
| Rada Społeczna — Przewodniczący (Starosta, z mocy ustawy) | Bogdan Zieliński | **PiS** — starosta, radny powiatu — **NIEZWERYFIKOWANE**, czy przewodniczy osobiście | https://www.portalsamorzadowy.pl/osoba/bogdan-zielinski,4307.html |
| Rada Społeczna — pozostali członkowie | **brak danych** | — | nieopublikowane online |
## 19. SP ZOZ w Sejnach (Szpital Powiatowy)
*Forma prawna: SPZOZ. Organ tworzący: Powiat Sejneński. Imienny skład Rady Społecznej 2024+ nie ustalony z jawnych źródeł (statut podaje tylko strukturę).*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Dyrektor | Waldemar Kwaterski | **Koalicja Obywatelska** — radny Sejmiku (od 2010), przew. Komisji Zdrowia | https://www.radio.bialystok.pl/gosc/suwalki/id/202941 |
| Z-ca ds. Lecznictwa | Urszula Ordowska | brak/niepotwierdzona | https://szpital.sejny.pl/kadra |
| Rada Społeczna — Przewodniczący (Starosta / osoba wyznaczona) | Maciej Plesiewicz | KW Ziemia Sejneńska (starosta) — **NIEZWERYFIKOWANE**, czy przewodniczy osobiście | https://www.suwalki24.pl/article/63,powiat-sejnenski-maciej-plesiewicz-nadal-starosta... |
| Rada Społeczna — pozostali członkowie | **brak danych** | — | uchwała nieodnaleziona online |
## 20. Szpital Powiatowy w Zambrowie sp. z o.o. ⚠️ SPÓŁKA — organ: RADA NADZORCZA
*Forma prawna: **spółka z o.o.** (KRS 0000130730), jedyny wspólnik: Powiat Zambrowski (100%). Organy: **Zarząd + Rada Nadzorcza** (nie rada społeczna).*
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL, dostęp 2026-07-06) |
|---|---|---|---|
| Prezes Zarządu (deleg. z RN od 16.07.2025, po rezygnacji Jana Bajno) | Kamil Kowaleczko | brak/niepotwierdzona | https://samorzad.gov.pl/web/powiat-zambrowski/... ; https://aleo.com/int/company/szpital-powiatowy-w-zambrowie... |
| Rada Nadzorcza — członek (od 9.10.2024) | Bartosz Rafał Ziemianowicz | **NIEZWERYFIKOWANE** — b. radny miasta (historycznie PO); źródła wtórne sprzeczne (PO vs PiS) | https://zambrowiacy.pl/info/bartosz-ziemianowicz/ |
| Rada Nadzorcza — członek (od 9.10.2024) | Krzysztof Rodzik | brak danych (nie mylić z „Krzysztof Rokicki") | https://aleo.com/int/company/szpital-powiatowy-w-zambrowie... |
| Rada Nadzorcza — członek (od 9.10.2024) | Michał Kulesza | brak danych | https://aleo.com/int/company/szpital-powiatowy-w-zambrowie... |
| Rada Nadzorcza — członek (od 27.11.2025) | Patrycja Magdalena Bugaj | brak danych | https://www.bizraport.pl/krs/0000130730/... |
| Przewodniczący Rady Nadzorczej | brak danych (KRS nie ujawnia funkcji wewnątrz RN) | — | j.w. |
---
# CZĘŚĆ III — Rozbieżności, luki i rekomendacje weryfikacyjne
## Rozbieżności źródeł (do rozstrzygnięcia przed publikacją)
- **Hajnówka — Andrzej Skiepko (przewodniczący):** dwa raporty podały różne komitety lokalne (KWW Ziemia Hajnowska vs KWW Porozumienie Samorządowe Regionu Puszczy Białowieskiej). Sprawdzić kartę PKW 2024.
- **Hajnówka — Jerzy Sirak (przedst. Wojewody):** afiliacja rozbieżna (KO vs historycznie SLD / komitet lokalny). Wymaga potwierdzenia.
- **Hajnówka — „Jerzy Wasiluk" figuruje przy dwóch gminach** (Czyże i Czeremcha) w odczycie uchwały III/13/2024 — możliwa zbieżność nazwisk lub artefakt renderowania. Sprawdzić oryginał PDF uchwały.
- **Siemiatycze — Mariusz Cieślik / Sylwester Zgierun:** „PSL" vs ostrożniejsze „KWW Ruch Ludowy". Przyjęto PSL za radio.bialystok.pl; do domknięcia w PKW.
- **Śniadeckiego (Białystok) — Karol Pilecki:** KO vs PO (źródło prasowe); historycznie PSL. Przyjęto KO.
- **Łomża — skład Rady Społecznej:** portal województwa (podlaskie.eu) opisywał wcześniej skład z Adamem Niebrzydowskim jako przewodniczącym — to **poprzednia kadencja**; aktualny (uchwała 44/866/2024) ma przewodniczącą Marię Konopkę. Rozbieżność wynika z różnych kadencji, nie z błędu.
## Główne luki (imienne składy rad społecznych nieopublikowane online)
Wymagają wniosku o informację publiczną lub wglądu w rejestr uchwał: **Choroszcz** (poza przewodniczącą), **Suwałki Rydygiera** (poza przewodniczącą), **Mońki**, **Sokółka** (7 z 11), **Wysokie Mazowieckie**, **Sejny**, **Augustów** (potwierdzenie aktualności po nowelizacji).
## Rekomendacje przed publikacją
1. **Kontrola krzyżowa afiliacji radnych sejmiku w PKW** (`samorzad2024.pkw.gov.pl`) — część afiliacji oparto na Wikipedii (źródło wtórne).
2. **Pozyskanie oryginałów uchwał** powołujących rady społeczne (BIP powiatów/urzędu marszałkowskiego) dla domknięcia luk i weryfikacji rozbieżności.
3. **Weryfikacja tożsamości przy dopasowaniach „NIEZWERYFIKOWANE"** — zwłaszcza tam, gdzie nazwisko jest pospolite (Możejko, Jabłoński, Dzięcioł, Niedźwiedzki, Wasiluk, Kwaterski/Kwaterski).
4. **Dane dynamiczne (stan na 2026-07-06)** wymagające odświeżenia tuż przed publikacją: dyrektor w Bielsku Podlaskim (wypowiedzenie z 29.06.2026), przejście Andrzeja Szewczuka z Siemiatycz do Choroszczy (VIII.2026), status prezesa w Zambrowie.
---
*Zestawienie oparte na źródłach publicznych (BIP, KRS/rejestr.io, PKW 2024, portalsamorzadowy.pl, media lokalne) dostępnych 2026-07-06. Dotyczy osób pełniących funkcje publiczne w związku z tymi funkcjami. Afiliacje oznaczone „NIEZWERYFIKOWANE" i „brak danych" nie powinny być publikowane jako fakty bez dodatkowego potwierdzenia.*
+3
View File
@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
+18
View File
@@ -0,0 +1,18 @@
services:
neo4j:
image: neo4j:5
container_name: neo4j-szpitale
ports:
- "7474:7474"
- "7687:7687"
environment:
- NEO4J_AUTH=neo4j/password
- NEO4J_apoc_export_file_enabled=true
- NEO4J_apoc_import_file_enabled=true
- NEO4J_apoc_import_file_use__neo4j__config=true
volumes:
- neo4j-data:/data
restart: unless-stopped
volumes:
neo4j-data:
+18
View File
@@ -0,0 +1,18 @@
{
"schemaVersion": "1.0",
"project": "szpitale-graph",
"description": "Stan wykonania kolejki zadań lokalnego agenta (PLAN2.md §11-13). Zrodlo prawdy przy konflikcie: git log.",
"resumeHint": "Wczytaj ten plik, znajdz pierwszy task o statusie todo/in_progress. Jesli in_progress i drzewo git brudne -> git checkout -- . && git clean -fd, ustaw task na todo, zacznij od zera (PLAN2.md §13.1).",
"createdAt": "2026-07-07",
"updatedAt": "2026-07-07",
"tasks": [
{ "id": "T1", "title": "Deduplicator compile fix", "tag": null, "status": "in_progress", "startedAt": "2026-07-07T00:00:00Z", "finishedAt": null, "note": "BLOKER: build czerwony az to zrobione" },
{ "id": "T2", "title": "NameNormalizer testy", "tag": null, "status": "todo", "startedAt": null, "finishedAt": null, "note": "" },
{ "id": "T3", "title": "PartyNormalizer testy", "tag": null, "status": "todo", "startedAt": null, "finishedAt": null, "note": "" },
{ "id": "T4", "title": "MarkdownRegistrySource test na fixture", "tag": null, "status": "todo", "startedAt": null, "finishedAt": null, "note": "" },
{ "id": "T5", "title": "Canonical write/read round-trip", "tag": null, "status": "todo", "startedAt": null, "finishedAt": null, "note": "" },
{ "id": "T6", "title": "GraphLoader idempotencja", "tag": "NEO4J", "status": "todo", "startedAt": null, "finishedAt": null, "note": "wymaga Dockera; bez -> skipped" },
{ "id": "T7", "title": "GraphValidator + overlap_report.csv", "tag": "NEO4J", "status": "todo", "startedAt": null, "finishedAt": null, "note": "wymaga Dockera; bez -> skipped" },
{ "id": "T8", "title": "Zielony pelny build", "tag": null, "status": "todo", "startedAt": null, "finishedAt": null, "note": "" }
]
}
Vendored Executable
+295
View File
@@ -0,0 +1,295 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"
+189
View File
@@ -0,0 +1,189 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.4
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+114
View File
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath/>
</parent>
<groupId>com.developx</groupId>
<artifactId>szpitale-graph</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>szpitale-graph</name>
<description>Neo4j graph application for Polish hospitals data</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Shell -->
<!-- Neo4j Spring Boot starter (version managed by parent BOM) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- Jsoup (HTML scraping) -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.18.3</version>
</dependency>
<!-- Commons Text (fuzzy matching) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.12.0</version>
</dependency>
<!-- Commons CSV (reports) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.12.0</version>
</dependency>
<!-- Lombok -->
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>neo4j</artifactId>
<version>1.20.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.20.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,12 @@
package com.developx.szpitale;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SzpitaleGraphApplication {
public static void main(String[] args) {
SpringApplication.run(SzpitaleGraphApplication.class, args);
}
}
@@ -0,0 +1,213 @@
package com.developx.szpitale.ingest;
import com.developx.szpitale.model.*;
import com.developx.szpitale.model.enums.*;
import com.developx.szpitale.ingest.normalize.NameNormalizer;
import com.developx.szpitale.ingest.normalize.PartyNormalizer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class CanonicalWriter {
private final ObjectMapper objectMapper = new ObjectMapper();
private final NameNormalizer nameNormalizer = new NameNormalizer();
private final PartyNormalizer partyNormalizer = new PartyNormalizer();
public CanonicalWriter() {
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
public void write(Path outputDir, List<RawHospitalRecord> rawRecords, String voivodeship,
IngestResults results) throws IOException {
Path canonicalDir = outputDir.resolve("canonical").normalize();
Files.createDirectories(canonicalDir);
List<Hospital> hospitals = new ArrayList<>();
List<Person> people = new ArrayList<>();
List<Role> roles = new ArrayList<>();
List<Affiliation> affiliations = new ArrayList<>();
for (RawHospitalRecord rawRecord : rawRecords) {
String hospitalId = nameNormalizer.makeHospitalId(voivodeship, rawRecord.hospitalName());
hospitals.add(new Hospital(
hospitalId,
rawRecord.hospitalName(),
rawRecord.shortName(),
rawRecord.city(),
voivodeship,
parseLegalForm(rawRecord.legalForm()),
rawRecord.foundingBody(),
parseSupervisoryBodyType(rawRecord.supervisoryBodyType()),
rawRecord.nip(),
rawRecord.krs(),
rawRecord.website(),
extractUniqueUrls(rawRecord.persons())
));
Map<String, Person> peopleMap = new LinkedHashMap<>();
Map<String, Set<String>> personUrlsMap = new LinkedHashMap<>();
for (RawHospitalRecord.RawPersonEntry personEntry : rawRecord.persons()) {
String fullName = personEntry.fullName();
if (fullName.isEmpty()) continue;
if (fullName.toLowerCase().contains("brak") &&
(personEntry.sourceUrl() == null || personEntry.sourceUrl().toLowerCase().contains("brak"))) {
continue;
}
String personId = nameNormalizer.makePersonId(fullName);
List<String> titles = nameNormalizer.extractTitles(fullName);
String extractedName = nameNormalizer.extractFullName(fullName);
if (!peopleMap.containsKey(personId)) {
peopleMap.put(personId, new Person(
personId, extractedName, personEntry.displayTitle(),
titles, new ArrayList<>()
));
personUrlsMap.put(personId, new LinkedHashSet<>());
}
if (personEntry.sourceUrl() != null && !personEntry.sourceUrl().isEmpty()) {
personUrlsMap.get(personId).add(personEntry.sourceUrl());
}
RoleType roleType = parseRoleType(personEntry.function());
OrganType organ = parseOrganType(personEntry.organ());
RoleStatus status = parseStatus(personEntry.statusNote());
roles.add(new Role(
hospitalId, personId, roleType,
personEntry.function(),
organ, status, null, null,
null,
List.of(personEntry.sourceUrl())
));
String affText = personEntry.partyAffiliation();
if (affText != null && !affText.isEmpty() &&
!affText.toLowerCase().contains("brak") &&
!affText.toLowerCase().contains("bezpartyj")) {
String canonicalParty = partyNormalizer.canonicalize(affText);
AffiliationType affType = partyNormalizer.determineType(affText);
ConfidenceLevel confidence = partyNormalizer.mapConfidence(personEntry.confidence());
String note = affText.contains("historycznie")
? "historycznie: " + affText
: affText;
affiliations.add(new Affiliation(
personId, affType, canonicalParty, confidence,
note,
List.of(personEntry.sourceUrl())
));
}
}
// Update people with merged URLs
List<Person> updatedPeople = new ArrayList<>();
for (Map.Entry<String, Set<String>> entry : personUrlsMap.entrySet()) {
Person p = peopleMap.get(entry.getKey());
if (p != null) {
updatedPeople.add(new Person(
p.id(), p.fullName(), p.displayName(),
p.titles(), new ArrayList<>(entry.getValue())
));
}
}
people.addAll(updatedPeople);
}
CanonicalDataset dataset = new CanonicalDataset(
"1.0", voivodeship, LocalDateTime.now(),
hospitals, people, affiliations, roles, List.of()
);
Path outputFile = canonicalDir.resolve(voivodeship + ".json");
objectMapper.writerWithDefaultPrettyPrinter().writeValue(outputFile.toFile(), dataset);
results.addHospitals(hospitals.size());
results.addPeople(people.size());
results.addRoles(roles.size());
results.addAffiliations(affiliations.size());
}
private List<String> extractUniqueUrls(List<RawHospitalRecord.RawPersonEntry> persons) {
return persons.stream()
.map(RawHospitalRecord.RawPersonEntry::sourceUrl)
.filter(url -> url != null && !url.isEmpty() && !url.toLowerCase().contains("brak"))
.distinct()
.collect(Collectors.toList());
}
private LegalForm parseLegalForm(String form) {
if (form == null) return LegalForm.SPZOZ;
String upper = form.toUpperCase();
if (upper.contains("SPOLKA")) return LegalForm.SPOLKA_Z_OO;
if (upper.contains("PSYCHIATRYCZNY")) return LegalForm.SP_PSYCHIATRYCZNY_ZOZ;
return LegalForm.SPZOZ;
}
private SupervisoryBodyType parseSupervisoryBodyType(String type) {
if (type == null) return SupervisoryBodyType.RADA_SPOLECZNA;
return "NADZORCZA".equalsIgnoreCase(type)
? SupervisoryBodyType.RADA_NADZORCZA
: SupervisoryBodyType.RADA_SPOLECZNA;
}
private RoleType parseRoleType(String function) {
String lower = function.toLowerCase();
if (lower.contains("dyrektor nacz")) {
return RoleType.DYREKTOR;
}
if (lower.contains("dyrektor") && !lower.contains("z-ca") && !lower.contains("zast")) {
return RoleType.DYREKTOR;
}
if (lower.contains("z-ca") || lower.contains("zast")) {
return RoleType.ZASTEPCA_DYREKTORA;
}
if (lower.contains("glown") || lower.contains("gl. ")) {
return RoleType.GLOWNY_KSIEGOWY;
}
if (lower.contains("naczel") && lower.contains("pieleg")) {
return RoleType.PRZELOZONA_PIELEGNIARKOW;
}
if (lower.contains("przewodnicz") && !lower.contains("z-ca przewodnich")) {
return RoleType.PRZEWODNICZACY_ORGANU;
}
if (lower.contains("sekte")) {
return RoleType.SEKRETARZ_ORGANU;
}
if (lower.contains("z-ca")) {
return RoleType.Z_CA_PRZEWODNICZACEGO;
}
return RoleType.CZLONEK_ORGANU_NADZORCZEGO;
}
private OrganType parseOrganType(String organ) {
if (organ == null) return OrganType.RADA_SPOLECZNA;
String upper = organ.toUpperCase();
if (upper.contains("DYREKCJA")) return OrganType.DYREKCJA;
if (upper.contains("NADZORCZA")) return OrganType.RADA_NADZORCZA;
return OrganType.RADA_SPOLECZNA;
}
private RoleStatus parseStatus(String note) {
if (note == null) return RoleStatus.AKTUALNY;
String lower = note.toLowerCase();
if (lower.contains("pelniac") || lower.contains("p.o.")) return RoleStatus.PELNIACY_OBOWIAZKI;
if (lower.contains("elekt")) return RoleStatus.ELEKT;
return RoleStatus.AKTUALNY;
}
}
@@ -0,0 +1,85 @@
package com.developx.szpitale.ingest;
import com.developx.szpitale.ingest.normalize.Deduplicator;
import com.developx.szpitale.ingest.source.MarkdownRegistrySource;
import com.developx.szpitale.ingest.source.VoivodeshipSource;
import com.developx.szpitale.model.RawHospitalRecord;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class IngestCommand {
public String ingest(String voivodeship, String source, String outDir, boolean split) {
if (!"markdown".equals(source)) {
return "Error: Only 'markdown' source is currently supported.";
}
Path outputDir = Path.of(outDir).toAbsolutePath().normalize();
Path researchDir = Path.of("data").toAbsolutePath().normalize();
List<VoivodeshipSource> sources = MarkdownRegistrySource.discoverAllSources(researchDir);
if (sources.isEmpty()) {
return "Error: No research-*.{md,markdown} files found in data/ directory.";
}
if (!"all".equals(voivodeship)) {
String target = voivodeship.toLowerCase();
sources = sources.stream()
.filter(s -> s.voivodeship().equals(target))
.collect(Collectors.toList());
if (sources.isEmpty()) {
return "Warning: No source found for voivodeship: " + voivodeship;
}
}
IngestResults totalResults = new IngestResults();
CanonicalWriter writer = new CanonicalWriter();
for (VoivodeshipSource src : sources) {
System.out.println("\nProcessing voivodeship: " + src.voivodeship());
List<RawHospitalRecord> records = src.fetch();
System.out.println(" Found " + records.size() + " hospital records");
IngestResults results = new IngestResults();
try {
writer.write(outputDir, records, src.voivodeship(), results);
System.out.println(" Written: " + outputDir.resolve("canonical").resolve(src.voivodeship() + ".json"));
} catch (IOException e) {
System.err.println(" Error writing: " + e.getMessage());
}
totalResults.addHospitals(results.getHospitalCount());
totalResults.addPeople(results.getPersonCount());
totalResults.addRoles(results.getRoleCount());
totalResults.addAffiliations(results.getAffiliationCount());
}
try {
writeIngestReport(outputDir, totalResults, sources);
} catch (IOException e) {
System.err.println("Error writing ingest report: " + e.getMessage());
}
return totalResults.toString();
}
private void writeIngestReport(Path outputDir, IngestResults results, List<VoivodeshipSource> sources) throws IOException {
Path reportPath = outputDir.resolve("ingest-report.json");
String report = "{" +
"\"schemaVersion\":\"1.0\"," +
"\"generatedAt\":\"" + java.time.LocalDateTime.now() + "\"," +
"\"voivodeshipsProcessed\":" + sources.size() + "," +
"\"hospitals\":" + results.getHospitalCount() + "," +
"\"people\":" + results.getPersonCount() + "," +
"\"roles\":" + results.getRoleCount() + "," +
"\"affiliations\":" + results.getAffiliationCount() + "," +
"\"gaps\":" + results.getGapCount() +
"}";
Files.writeString(reportPath, report);
System.out.println("\nIngest report written to: " + reportPath);
}
}
@@ -0,0 +1,29 @@
package com.developx.szpitale.ingest;
public class IngestResults {
private int hospitalCount = 0;
private int personCount = 0;
private int roleCount = 0;
private int affiliationCount = 0;
private int gapCount = 0;
public void addHospitals(int count) { hospitalCount += count; }
public void addPeople(int count) { personCount += count; }
public void addRoles(int count) { roleCount += count; }
public void addAffiliations(int count) { affiliationCount += count; }
public void addGaps(int count) { gapCount += count; }
public int getHospitalCount() { return hospitalCount; }
public int getPersonCount() { return personCount; }
public int getRoleCount() { return roleCount; }
public int getAffiliationCount() { return affiliationCount; }
public int getGapCount() { return gapCount; }
@Override
public String toString() {
return String.format(
"Ingest results: %d hospitals, %d people, %d roles, %d affiliations, %d gaps",
hospitalCount, personCount, roleCount, affiliationCount, gapCount
);
}
}
@@ -0,0 +1,82 @@
package com.developx.szpitale.ingest.normalize;
import com.developx.szpitale.model.Person;
import org.apache.commons.text.similarity.JaroWinklerSimilarity;
import java.util.*;
import java.util.stream.Collectors;
public class Deduplicator {
private static final double DUPLICATE_THRESHOLD = 0.85;
private static final double SUSPECT_THRESHOLD = 0.70;
private final NameNormalizer nameNormalizer = new NameNormalizer();
public List<Person> deduplicate(List<Person> people) {
List<String> personIds = new ArrayList<>();
Map<String, List<Person>> bySlug = people.stream()
.collect(Collectors.groupingBy(Person::id));
List<Person> result = new ArrayList<>();
Set<String> processed = new HashSet<>();
for (Map.Entry<String, List<Person>> entry : bySlug.entrySet()) {
List<Person> group = entry.getValue();
if (group.size() == 1) {
result.add(group.get(0));
} else {
// Same slug - merge source URLs
Person merged = mergeBySlug(group);
result.add(merged);
}
}
// Check cross-slug candidates with fuzzy matching
checkFuzzyDuplicates(result);
return result;
}
private Person mergeBySlug(List<Person> duplicates) {
Person first = duplicates.get(0);
Set<String> allUrls = duplicates.stream()
.flatMap(p -> p.sourceUrls().stream())
.collect(Collectors.toSet());
return new Person(
first.id(),
first.fullName(),
first.displayName(),
first.titles(),
new ArrayList<>(allUrls)
);
}
private void checkFuzzyDuplicates(List<Person> people) {
Map<String, List<String>> groups = new HashMap<>();
for (int i = 0; i < people.size(); i++) {
Person person = people.get(i);
List<String> candidates = new ArrayList<>();
for (int j = 0; j < i; j++) {
Person existing = people.get(j);
double similarity = nameNormalizer.fuzzySimilarity(
person.fullName(),
existing.fullName()
);
if (similarity >= SUSPECT_THRESHOLD && similarity < DUPLICATE_THRESHOLD) {
candidates.add(existing.id());
}
}
if (!candidates.isEmpty()) {
groups.put(person.id(), candidates);
}
}
if (!groups.isEmpty()) {
System.out.println("Warning: Possible name similarity conflicts (manual review recommended):");
groups.forEach((id, candidates) ->
System.out.println(" " + id + " -> similar to: " + String.join(", ", candidates)));
}
}
}
@@ -0,0 +1,73 @@
package com.developx.szpitale.ingest.normalize;
import org.apache.commons.text.similarity.JaroWinklerSimilarity;
import java.text.Normalizer;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class NameNormalizer {
private static final Set<String> TITLE_TOKENS = Set.of(
"prof.", "prof", "dr hab.", "dr.hab.", "dr.", "mgr", "mgr in\u017c.", "in\u017c.",
"licencjat in\u017c.", "lek.", "lek.med.", "n. med.", "n. o zdr."
);
private final JaroWinklerSimilarity jaroWinkler = new JaroWinklerSimilarity();
public String makeSlug(String fullName) {
String normalized = Normalizer.normalize(fullName, Normalizer.Form.NFKD);
normalized = normalized.replaceAll("\\p{M}", "");
normalized = normalized.replaceAll("[^a-zA-Z0-9\\s-]", " ");
normalized = normalized.toLowerCase().replaceAll("\\s+", "-").replaceAll("-+", "-");
normalized = normalized.replaceAll("^-|-$", "");
return normalized;
}
public String extractFullName(String rawName) {
return extractGivenAndFamilyNames(rawName);
}
public List<String> extractTitles(String rawName) {
List<String> titles = new ArrayList<>();
String lower = rawName.toLowerCase();
for (String title : TITLE_TOKENS) {
if (lower.contains(title.toLowerCase())) {
titles.add(title.trim());
}
}
return titles;
}
public String makePersonId(String fullName) {
String slug = makeSlug(fullName);
return "person:" + slug;
}
public String makeHospitalId(String voivodeship, String hospitalName) {
String slug = makeSlug(hospitalName);
return "hosp:" + voivodeship + ":" + slug;
}
public double fuzzySimilarity(String a, String b) {
return jaroWinkler.apply(a.toLowerCase(), b.toLowerCase());
}
public List<String> findDuplicateCandidates(List<String> names, String targetName, double threshold) {
return names.stream()
.filter(name -> fuzzySimilarity(name, targetName) >= threshold)
.filter(name -> !name.equals(targetName))
.collect(Collectors.toList());
}
private String extractGivenAndFamilyNames(String rawName) {
Pattern pattern = Pattern.compile(
"(?i)(prof\\.\\s*|dr\\s*hab\\.\\s*|dr\\.\\s*|mgr\\s*|in\u017c\\.\\s*)",
Pattern.MULTILINE | Pattern.DOTALL
);
Matcher matcher = pattern.matcher(rawName);
return matcher.replaceAll("").trim();
}
}
@@ -0,0 +1,64 @@
package com.developx.szpitale.ingest.normalize;
import com.developx.szpitale.model.enums.AffiliationType;
import com.developx.szpitale.model.enums.ConfidenceLevel;
import java.util.Map;
import java.util.Set;
public class PartyNormalizer {
private static final Map<String, String> PARTY_CANONICAL_MAPPINGS = Map.ofEntries(
Map.entry("koalicja obywatelska", "Koalicja Obywatelska"),
Map.entry("po", "Koalicja Obywatelska"),
Map.entry("polska oferta", "Koalicja Obywatelska"),
Map.entry("prawo i sprawiedliwosc", "Prawo i Sprawiedliwo\u015b\u0107"),
Map.entry("pis", "Prawo i Sprawiedliwo\u015b\u0107"),
Map.entry("psl", "PSL"),
Map.entry("polska 2050", "Polska 2050"),
Map.entry("trzecia droga", "Trzecia Droga"),
Map.entry("polska 2050 pisl", "Trzecia Droga"),
Map.entry("sl d", "SLD"),
Map.entry("lewica", "Lewica"),
Map.entry("prawica rzeczpospolitej", "Prawica Rzeczypospolitej"),
Map.entry("ruch ludowy", "Ruch Ludowy"),
Map.entry("kww", "KWW_LOKALNY")
);
private static final Set<String> KOMMIT_SET = Set.of(
"kww", "komitet wyborczy", "komitet lokalny", "komitet wyborc\u00f3w"
);
public String canonicalize(String rawName) {
String lower = rawName.toLowerCase().trim();
if (PARTY_CANONICAL_MAPPINGS.containsKey(lower)) {
return PARTY_CANONICAL_MAPPINGS.get(lower);
}
for (Map.Entry<String, String> entry : PARTY_CANONICAL_MAPPINGS.entrySet()) {
if (lower.contains(entry.getKey())) {
return entry.getValue();
}
}
return rawName;
}
public AffiliationType determineType(String rawName) {
String lower = rawName.toLowerCase();
if (lower.contains("kww") || lower.contains("komitet wyborc\u00f3w")) {
return AffiliationType.KWW_LOKALNY;
}
if (lower.contains("komitet")) {
return AffiliationType.KOMITET_WYBORCZY;
}
return AffiliationType.PARTIA;
}
public ConfidenceLevel mapConfidence(String confidenceText) {
if (confidenceText == null) return ConfidenceLevel.BRAK_DANYCH;
String lower = confidenceText.toLowerCase();
if (lower.contains("brak danych")) return ConfidenceLevel.BRAK_DANYCH;
if (lower.contains("niezweryfik")) return ConfidenceLevel.NIEZWERYFIKOWANA;
if (lower.contains("niepotwierdzona")) return ConfidenceLevel.NIEPOTWIERDZONA;
return ConfidenceLevel.POTWIERDZONA;
}
}
@@ -0,0 +1,361 @@
package com.developx.szpitale.ingest.source;
import com.developx.szpitale.model.RawHospitalRecord;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public class MarkdownRegistrySource implements VoivodeshipSource {
private static final Logger log = LoggerFactory.getLogger(MarkdownRegistrySource.class);
private final Path researchDir;
private final String voivodeshipSlug;
public MarkdownRegistrySource(Path researchDir, String voivodeshipSlug) {
this.researchDir = researchDir;
this.voivodeshipSlug = voivodeshipSlug;
}
@Override
public String voivodeship() {
return voivodeshipSlug;
}
@Override
public List<RawHospitalRecord> fetch() {
Path researchFile = findResearchFile(voivodeshipSlug);
if (researchFile == null) {
log.warn("No research file found for voivodeship: {}", voivodeshipSlug);
return List.of();
}
return parseMarkdownFile(researchFile);
}
private Path findResearchFile(String voivodeship) {
Path path = researchDir.resolve("research-" + voivodeship + ".md");
if (Files.exists(path)) {
return path;
}
path = researchDir.resolve("research-" + voivodeship + ".markdown");
if (Files.exists(path)) {
return path;
}
return null;
}
private List<RawHospitalRecord> parseMarkdownFile(Path filePath) {
List<RawHospitalRecord> records = new ArrayList<>();
try {
String content = Files.readString(filePath);
parseContent(content, records);
} catch (IOException e) {
log.error("Failed to read research file: {}", filePath, e);
}
return records;
}
private void parseContent(String content, List<RawHospitalRecord> records) {
String[] sections = content.split("## \\d+\\. ");
for (int i = 1; i < sections.length; i++) {
RawHospitalRecord record = parseHospitalSection(sections[i]);
if (record != null) {
records.add(record);
}
}
// Also try to parse with just "## " prefix (fallback)
if (records.isEmpty()) {
String[] fallbackSections = content.split("## ");
for (int i = 1; i < fallbackSections.length; i++) {
if (!fallbackSections[i].contains("Nota metodologiczna")
&& !fallbackSections[i].contains("Rozbiezności")
&& !fallbackSections[i].contains("Główne luki")
&& !fallbackSections[i].contains("Rekomendacje")) {
RawHospitalRecord record = parseHospitalSection(fallbackSections[i]);
if (record != null && record.hospitalName() != null && !record.hospitalName().contains("Nota")) {
records.add(record);
}
}
}
}
}
private RawHospitalRecord parseHospitalSection(String section) {
String[] lines = section.split("\n", 3);
if (lines.length < 2) return null;
String headerLine = lines[0].trim();
String hospitalName = headerLine.replace("*", "").trim();
if (hospitalName.isEmpty() || hospitalName.startsWith("|")) return null;
String body = lines.length > 2 ? lines[2] : "";
String legalForm = extractLegalForm(body);
String foundingBody = extractFoundingBody(body);
String supervisoryBodyType = extractSupervisoryBodyType(body);
List<RawHospitalRecord.RawPersonEntry> persons = parsePersonTables(section, legalForm, supervisoryBodyType,
extractWebsite(section), hospitalName);
boolean hasData = persons.stream().anyMatch(p -> p.sourceUrl() != null && !p.sourceUrl().isEmpty() && !p.sourceUrl().contains("brak"));
return new RawHospitalRecord(
hospitalName,
extractShortName(hospitalName),
extractCity(hospitalName),
voivodeship(),
legalForm,
foundingBody,
supervisoryBodyType,
null,
extractKrs(body),
extractWebsite(headerLine + "\n" + body),
persons
);
}
private List<RawHospitalRecord.RawPersonEntry> parsePersonTables(String section, String defaultLegalForm,
String defaultSupervisory, String defaultWebsite, String hospitalName) {
List<RawHospitalRecord.RawPersonEntry> entries = new ArrayList<>();
Document doc = Jsoup.parse(section);
Elements tables = doc.select("table");
for (Element table : tables) {
Elements rows = table.select("tr");
if (rows.size() < 2) continue;
Elements headerCells = rows.get(0).select("th, td");
boolean isPersonTable = headerCells.stream().anyMatch(h ->
h.text().contains("Funkcja") && h.text().contains("Imię"));
if (!isPersonTable) continue;
for (int i = 1; i < rows.size(); i++) {
Element row = rows.get(i);
Elements cells = row.select("td");
if (cells.isEmpty()) continue;
if (cells.get(0).text().contains("brak danych") ||
cells.get(0).text().contains("brak imiennego")) {
break;
}
String function = cleanText(cells.get(0).html());
String fullName = cleanText(cells.get(1).html());
String affiliation = cells.size() > 2 ? cleanText(cells.get(2).html()) : "brak/niepotwierdzona";
String sourceUrl = cells.size() > 3 ? extractUrl(cells.get(3).html()) : "";
if (fullName.isEmpty() || "brak".equalsIgnoreCase(fullName)) continue;
if (function.contains("brak") && fullName.contains("brak danych")) continue;
String organ = detectOrgan(function, defaultSupervisory, defaultLegalForm);
String confidence = mapConfidence(affiliation);
String status = mapStatus(function);
// Extract display title from fullName
String displayTitle = extractDisplayTitle(fullName, function);
entries.add(new RawHospitalRecord.RawPersonEntry(
function, fullName, displayTitle, affiliation,
confidence, sourceUrl, organ, status
));
}
}
if (entries.isEmpty()) {
// Fallback: try to parse single-line entries
parseFallbackEntries(section, entries, defaultSupervisory, defaultLegalForm);
}
return entries;
}
private void parseFallbackEntries(String section, List<RawHospitalRecord.RawPersonEntry> entries,
String defaultSupervisory, String defaultLegalForm) {
String[] lines = section.split("\n");
Pattern personPattern = Pattern.compile(
"^\\s*\\|\\s*(Dyrektor|Z-ca|p\\.o\\.|Rada|Naczelna|Główne)\\b[^|]*\\|\\s*([^|]+)\\|([^|]*)\\|([^|]*)\\s*$",
Pattern.MULTILINE
);
for (String line : lines) {
Matcher matcher = personPattern.matcher(line);
if (matcher.find()) {
String function = cleanText(matcher.group(1));
String fullName = cleanText(matcher.group(2)).trim();
String affiliation = cleanText(matcher.group(3)).trim();
String sourceUrl = cleanText(matcher.group(4)).trim();
if (!fullName.isEmpty()) {
entries.add(new RawHospitalRecord.RawPersonEntry(
function, fullName, null, affiliation,
mapConfidence(affiliation), sourceUrl,
detectOrgan(function, defaultSupervisory, defaultLegalForm), ""
));
}
}
}
}
private String detectOrgan(String function, String defaultSupervisory, String defaultLegalForm) {
String lower = function.toLowerCase();
if (lower.contains("dyrektor") && !lower.contains("z-ca") && !lower.contains("p.o.")) {
return "DYREKCJA";
}
if (lower.contains("z-ca") || lower.contains("p.o.") ||
lower.contains("główn") || lower.contains("naczel") ||
lower.contains("przel")) {
return "DYREKCJA";
}
if (lower.contains("rada")) {
if (defaultSupervisory != null && defaultSupervisory.contains("NADZORCZA")) {
return "RADA_NADZORCZA";
}
return "RADA_SPOLECZNA";
}
if (defaultSupervisory != null && defaultSupervisory.contains("NADZORCZA")) {
return "RADA_NADZORCZA";
}
return "RADA_SPOLECZNA";
}
private String extractLegalForm(String body) {
if (body.contains("sp. z o.o.") || body.contains("spółka")) {
return "SPOLKA_Z_OO";
}
if (body.contains("Psychiatryczny")) {
return "SP_PSYCHIATRYCZNY_ZOZ";
}
return "SPZOZ";
}
private String extractFoundingBody(String body) {
var mat = Pattern.compile("Organ tworzący:\\s*(.+?)\\.(?:\\s|$)", Pattern.MULTILINE | Pattern.DOTALL).matcher(body);
if (mat.find()) {
return cleanText(mat.group(1)).trim();
}
return null;
}
private String extractSupervisoryBodyType(String body) {
if (body.contains("Rada Nadzorcza") || body.contains("RADA NADZORCZA")) {
return "RADA_NADZORCZA";
}
return "RADA_SPOLECZNA";
}
private String extractKrs(String body) {
var mat = Pattern.compile("(?:KRS\\s*|krs\\s*:\\s*)(\\d{8,10})").matcher(body);
if (mat.find()) {
return mat.group(1);
}
return null;
}
private String extractWebsite(String text) {
var mat = Pattern.compile("(https?://[^\\s|]+)").matcher(text);
if (mat.find()) {
return mat.group(1);
}
return null;
}
private String extractShortName(String fullName) {
if (fullName.contains("Kliniczny") && fullName.contains("w ")) {
var mat = Pattern.compile("([A-Z]{2,4})\\b")
.matcher(fullName.split("Kliniczny")[0]);
if (mat.find()) return "USK";
}
return null;
}
private String extractCity(String hospitalName) {
var mat = Pattern.compile("w\\s+([^,.(]+)").matcher(hospitalName);
if (mat.find()) {
return cleanText(mat.group(1)).trim();
}
return null;
}
private String extractDisplayTitle(String fullName, String function) {
if (function.contains("Dyrektor Naczelny")) {
return "Dyrektor Naczelny";
}
if (function.contains("p.o.")) {
return "p.o. " + function.replace("p.o. ", "");
}
return null;
}
private String mapConfidence(String affiliation) {
if (affiliation == null) return "BRAK_DANYCH";
String lower = affiliation.toLowerCase();
if (lower.contains("brak danych")) return "BRAK_DANYCH";
if (lower.contains("niezweryfik") || lower.contains("NIEZWERYFIKOWANE")) return "NIEZWERYFIKOWANA";
if (lower.contains("brak/niepotwierdzona") || lower.contains("niepotwierdzona") ||
lower.contains("niepotwierdzone") || lower.contains("niepotwierdzony")) return "NIEPOTWIERDZONA";
if (lower.contains("NIEZWERYFIKOWANE (zbieżność") || lower.contains("nieweryfikow")) return "NIEZWERYFIKOWANA";
if (affiliation.contains("KO") || affiliation.contains("PO") ||
affiliation.contains("PiS") || affiliation.contains("PSL") ||
affiliation.contains("Trzecia Droga") || affiliation.contains("Polska 2050") ||
affiliation.contains("KWW ") || affiliation.contains("KOMITET") ||
affiliation.contains("Prawica") || affiliation.contains("SLD") ||
affiliation.contains("Lewica")) {
return "POTWIERDZONA";
}
return "NIEPOTWIERDZONA";
}
private String mapStatus(String function) {
String lower = function.toLowerCase();
if (lower.contains("p.o.") || lower.contains("pełniący")) return "PELNIACY_OBOWIAZKI";
if (lower.contains("elekt")) return "ELEKT";
if (lower.contains("były") || lower.contains("dawniej")) return "BYLY";
return "AKTUALNY";
}
private String cleanText(String html) {
if (html == null) return "";
Document doc = Jsoup.parse(html);
String text = doc.text().trim();
// Remove trailing pipe if any
if (text.endsWith("|")) text = text.substring(0, text.length() - 1).trim();
return text;
}
private String extractUrl(String text) {
String cleaned = cleanText(text);
var mat = Pattern.compile("(https?://[^\\s]+)").matcher(cleaned);
if (mat.find()) return mat.group(1).replaceAll("[)]+$", "");
return cleaned.isEmpty() ? "" : cleaned;
}
public static List<VoivodeshipSource> discoverAllSources(Path researchDir) {
List<VoivodeshipSource> sources = new ArrayList<>();
try (var paths = Files.list(researchDir)) {
paths.filter(p -> p.toString().endsWith(".md"))
.filter(p -> p.getFileName().toString().startsWith("research-"))
.forEach(p -> {
String voiv = p.getFileName().toString().replace("research-", "").replace(".md", "");
sources.add(new MarkdownRegistrySource(researchDir, voiv));
log.info("Discovered research source for voivodeship: {}", voiv);
});
} catch (IOException e) {
log.warn("Could not scan research directory: {}", researchDir);
}
return sources;
}
}
@@ -0,0 +1,10 @@
package com.developx.szpitale.ingest.source;
import com.developx.szpitale.model.RawHospitalRecord;
import java.util.List;
public interface VoivodeshipSource {
String voivodeship();
List<RawHospitalRecord> fetch();
}
@@ -0,0 +1,53 @@
package com.developx.szpitale.load;
import com.developx.szpitale.model.CanonicalDataset;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class CanonicalReader {
private final ObjectMapper objectMapper = new ObjectMapper();
public CanonicalReader() {
objectMapper.registerModule(new JavaTimeModule());
}
public List<CanonicalDataset> readAll(Path inputDir, String voivodeship) throws IOException {
List<CanonicalDataset> datasets = new ArrayList<>();
Path canonicalDir = inputDir.resolve("canonical").toAbsolutePath().normalize();
if (!Files.exists(canonicalDir)) {
System.err.println("Error: canonical directory not found: " + canonicalDir);
return datasets;
}
try (var paths = Files.list(canonicalDir)) {
paths.filter(p -> p.getFileName().toString().endsWith(".json"))
.filter(p -> {
if ("all".equalsIgnoreCase(voivodeship)) return true;
String base = p.getFileName().toString().replace(".json", "");
return base.equalsIgnoreCase(voivodeship);
})
.forEach(p -> {
try {
CanonicalDataset ds = objectMapper.readValue(p.toFile(), CanonicalDataset.class);
datasets.add(ds);
} catch (IOException e) {
System.err.println("Error reading " + p + ": " + e.getMessage());
}
});
}
return datasets;
}
public CanonicalDataset readSingle(Path inputDir, String voivodeship) throws IOException {
return readAll(inputDir, voivodeship).stream().findFirst().orElse(null);
}
}
@@ -0,0 +1,216 @@
package com.developx.szpitale.load;
import com.developx.szpitale.model.*;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
public class GraphLoader {
private static final Logger log = LoggerFactory.getLogger(GraphLoader.class);
private static final int BATCH_SIZE = 1000;
private final Driver driver;
public GraphLoader(Driver driver) {
this.driver = driver;
}
public LoadStats load(List<CanonicalDataset> datasets) {
LoadStats stats = new LoadStats();
try (Session session = driver.session()) {
for (CanonicalDataset dataset : datasets) {
stats.addVoivodeship(dataset.voivodeship());
loadHospitals(session, dataset.hospitals(), stats);
loadPeople(session, dataset.people(), stats);
loadRoles(session, dataset.roles(), stats);
loadAffiliations(session, dataset.affiliations(), stats);
loadMandates(session, dataset.mandates(), stats);
}
}
return stats;
}
private void loadHospitals(Session session, List<Hospital> hospitals, LoadStats stats) {
List<Map<String, Object>> batch = new ArrayList<>();
for (Hospital h : hospitals) {
batch.add(Map.of(
"id", h.id(),
"name", h.name(),
"shortName", h.shortName(),
"city", h.city(),
"voivodeship", h.voivodeship(),
"legalForm", h.legalForm() != null ? h.legalForm().name() : null,
"foundingBody", h.foundingBody(),
"supervisoryBodyType", h.supervisoryBodyType() != null ? h.supervisoryBodyType().name() : null,
"website", h.website(),
"sourceUrls", h.sourceUrls()
));
}
for (int i = 0; i < batch.size(); i += BATCH_SIZE) {
List<Map<String, Object>> subList = batch.subList(i, Math.min(i + BATCH_SIZE, batch.size()));
String cypher = "UNWIND $batch AS h " +
"MERGE (hospital:Hospital {id: h.id}) " +
"SET hospital += {name:h.name, shortName:h.shortName, city:h.city, " +
" voivodeship:h.voivodeship, legalForm:h.legalForm, " +
" foundingBody:h.foundingBody, supervisoryBodyType:h.supervisoryBodyType, " +
" website:h.website} " +
"MERGE (v:Voivodeship {slug: h.voivodeship}) " +
"MERGE (hospital)-[:W_WOJEWODZTWIE]->(v)";
session.run(cypher, Map.of("batch", subList));
stats.addHospitals(subList.size());
}
}
private void loadPeople(Session session, List<Person> people, LoadStats stats) {
List<Map<String, Object>> batch = new ArrayList<>();
for (Person p : people) {
batch.add(Map.of(
"id", p.id(),
"fullName", p.fullName(),
"displayName", p.displayName(),
"titles", p.titles() != null ? p.titles() : Collections.emptyList(),
"sourceUrls", p.sourceUrls()
));
}
for (int i = 0; i < batch.size(); i += BATCH_SIZE) {
List<Map<String, Object>> subList = batch.subList(i, Math.min(i + BATCH_SIZE, batch.size()));
String cypher = "UNWIND $batch AS p " +
"MERGE (person:Person {id: p.id}) " +
"SET person += {fullName:p.fullName, displayName:p.displayName, " +
" titles:p.titles, sourceUrls:p.sourceUrls}";
session.run(cypher, Map.of("batch", subList));
stats.addPeople(subList.size());
}
}
private void loadRoles(Session session, List<Role> roles, LoadStats stats) {
Map<String, List<Role>> byRelType = new LinkedHashMap<>();
for (Role r : roles) {
String relType = roleTypeToRelName(r);
byRelType.computeIfAbsent(relType, k -> new ArrayList<>()).add(r);
}
for (Map.Entry<String, List<Role>> entry : byRelType.entrySet()) {
String relType = entry.getKey();
List<Role> items = entry.getValue();
for (int i = 0; i < items.size(); i += BATCH_SIZE) {
List<Role> subList = items.subList(i, Math.min(i + BATCH_SIZE, items.size()));
String cypher = "UNWIND $batch AS r " +
"MATCH (h:Hospital {id: r.hospitalId}) " +
"MATCH (p:Person {id: r.personId}) " +
"MERGE (h)-[rrel:`" + escapeBacktick(relType) + "`]->(p) " +
"SET rrel += {roleType: r.roleType, roleLabel: r.roleLabel, " +
" organ: r.organ, status: r.status, " +
" validFrom: r.validFrom, validTo: r.validTo, note: r.note}";
session.run(cypher, Map.of("batch", subList.stream()
.map(r -> Map.of(
"hospitalId", r.hospitalId(),
"personId", r.personId(),
"roleType", r.roleType().name(),
"roleLabel", r.roleLabel(),
"organ", r.organ().name(),
"status", r.status().name(),
"validFrom", r.validFrom(),
"validTo", r.validTo(),
"note", r.note()
))
.collect(Collectors.toList())));
stats.addRoles(subList.size());
}
}
}
private void loadAffiliations(Session session, List<Affiliation> affiliations, LoadStats stats) {
for (int i = 0; i < affiliations.size(); i += BATCH_SIZE) {
List<Affiliation> subList = affiliations.subList(
i, Math.min(i + BATCH_SIZE, affiliations.size()));
String cypher = "UNWIND $batch AS a " +
"MATCH (p:Person {id: a.personId}) " +
"MERGE (party:Party {name: a.name}) SET party.type = a.type " +
"MERGE (p)-[m:CZLONEK_PARTII]->(party) " +
"SET m += {confidence: a.confidence, note: a.note, sourceUrls: a.sourceUrls}";
session.run(cypher, Map.of("batch", subList.stream()
.map(a -> Map.of(
"personId", a.personId(),
"name", a.name(),
"type", a.type().name(),
"confidence", a.confidence().name(),
"note", a.note(),
"sourceUrls", a.sourceUrls()
))
.collect(Collectors.toList())));
stats.addAffiliations(subList.size());
}
}
private void loadMandates(Session session, List<Mandate> mandates, LoadStats stats) {
if (mandates.isEmpty()) return;
for (int i = 0; i < mandates.size(); i += BATCH_SIZE) {
List<Mandate> subList = mandates.subList(
i, Math.min(i + BATCH_SIZE, mandates.size()));
String cypher = "UNWIND $batch AS m " +
"MATCH (p:Person {id: m.personId}) " +
"MERGE (g:GovBody {name: m.body}) " +
" SET g.type = m.mandateType, g.voivodeship = m.voivodeship, g.term = m.term " +
"MERGE (p)-[:PELNI_MANDAT {mandateType: m.mandateType, term: m.term}]->(g)";
session.run(cypher, Map.of("batch", subList.stream()
.map(m -> Map.of(
"personId", m.personId(),
"body", m.body(),
"mandateType", m.mandateType().name(),
"term", m.term(),
"voivodeship", m.voivodeship(),
"sourceUrls", m.sourceUrls()
))
.collect(Collectors.toList())));
stats.addMandates(subList.size());
}
}
private String roleTypeToRelName(Role r) {
String organ = r.organ().name();
String role = r.roleType().name();
if ("DYREKCJA".equals(organ) && "DYREKTOR".equals(role)) return "DYREKTOR";
if ("DYREKCJA".equals(organ) && ("ZASTEPCA_DYREKTORA".equals(role) || "GLOWNY_KSIEGOWY".equals(role) || "PRZELOZONA_PIELEGNIARKOW".equals(role))) return "ZASTEPCA_DYREKTORA";
if ("PRZEWODNICZACY_ORGANU".equals(role)) return "PRZEWODNICZY_ORGANOWI";
return "CZLONEK_ORGANU";
}
private String escapeBacktick(String s) {
return s.replace("`", "\\`");
}
public static class LoadStats {
public int hospitalCount;
public int personCount;
public int roleCount;
public int affiliationCount;
public int mandateCount;
public final List<String> voivodeships = new ArrayList<>();
public void addVoivodeship(String v) { voivodeships.add(v); }
public void addHospitals(int count) { hospitalCount += count; }
public void addPeople(int count) { personCount += count; }
public void addRoles(int count) { roleCount += count; }
public void addAffiliations(int count) { affiliationCount += count; }
public void addMandates(int count) { mandateCount += count; }
@Override
public String toString() {
return String.format(
"Load stats - voivodeships: %s, hospitals: %d, people: %d, roles: %d, affiliations: %d, mandates: %d",
String.join(", ", voivodeships), hospitalCount, personCount, roleCount, affiliationCount, mandateCount
);
}
}
}
@@ -0,0 +1,36 @@
package com.developx.szpitale.load;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GraphSchema {
private static final Logger log = LoggerFactory.getLogger(GraphSchema.class);
private final Driver driver;
public GraphSchema(Driver driver) {
this.driver = driver;
}
public void createConstraints() {
try (Session session = driver.session()) {
session.run("CREATE CONSTRAINT hospital_id IF NOT EXISTS FOR (h:Hospital) REQUIRE h.id IS UNIQUE");
session.run("CREATE CONSTRAINT person_id IF NOT EXISTS FOR (p:Person) REQUIRE p.id IS UNIQUE");
session.run("CREATE CONSTRAINT party_name IF NOT EXISTS FOR (x:Party) REQUIRE x.name IS UNIQUE");
session.run("CREATE CONSTRAINT voiv_slug IF NOT EXISTS FOR (v:Voivodeship) REQUIRE v.slug IS UNIQUE");
session.run("CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.fullName)");
session.run("CREATE INDEX govbody_name IF NOT EXISTS FOR (g:GovBody) ON (g.name)");
log.info("Graph constraints and indexes created");
}
}
public void dropAll() {
try (Session session = driver.session()) {
session.run("MATCH (n) DETACH DELETE n");
log.info("All nodes and relationships deleted");
}
}
}
@@ -0,0 +1,126 @@
package com.developx.szpitale.load;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public class GraphValidator {
private static final Logger log = LoggerFactory.getLogger(GraphValidator.class);
private final Driver driver;
public GraphValidator(Driver driver) {
this.driver = driver;
}
public ValidationReport validate() {
ValidationReport report = new ValidationReport();
try (Session session = driver.session()) {
// 1. Hospitals without directors
var result1 = session.run(
"MATCH (h:Hospital) WHERE NOT (h)-[:DYREKTOR]->() " +
"RETURN h.name AS hospital, h.city AS city, h.voivodeship AS voivodeship " +
"ORDER BY h.voivodeship, h.name");
List<Map<String, Object>> hospitalsWithoutDirector = new ArrayList<>();
result1.list().forEach(r -> hospitalsWithoutDirector.add(r.asMap()));
report.setHospitalsWithoutDirector(hospitalsWithoutDirector);
// 2. People with mandates linking to hospital roles
var result2 = session.run(
"MATCH (h:Hospital)-[r:CZLONEK_ORGANU|PRZEWODNICZY_ORGANOWI]->(p:Person)-[:PELNI_MANDAT]->(g:GovBody) " +
"RETURN p.fullName AS person, " +
" collect(DISTINCT h.name) AS szpitale, " +
" collect(DISTINCT g.name) AS mandaty, " +
" collect(DISTINCT labels(p)) AS labels " +
"ORDER BY person");
List<Map<String, Object>> overlaps = new ArrayList<>();
result2.list().forEach(r -> overlaps.add(r.asMap()));
report.setOverlapReport(overlaps);
// 3. People in multiple hospitals
var result3 = session.run(
"MATCH (h:Hospital)-[r:CZLONEK_ORGANU|PRZEWODNICZY_ORGANOWI]->(p:Person) " +
"WITH p, collect(DISTINCT h.name) AS szpitale WHERE size(szpitale) > 1 " +
"RETURN p.fullName AS person, szpitale, count(p) AS connections " +
"ORDER BY connections DESC");
List<Map<String, Object>> multiHospital = new ArrayList<>();
result3.list().forEach(r -> multiHospital.add(r.asMap()));
report.setMultiHospitalReport(multiHospital);
// 4. Overall counts
var nodeCountResult = session.run(
"MATCH (n) RETURN labels(n)[0] AS label, count(*) AS count ORDER BY count DESC");
Map<String, Long> nodeCounts = new HashMap<>();
nodeCountResult.list().forEach(r ->
nodeCounts.put(r.get("label").asString(), r.get("count").asLong()));
report.setNodeCounts(nodeCounts);
var relCountResult = session.run(
"MATCH ()-[rel]->() RETURN type(rel) AS type, count(*) AS count ORDER BY count DESC");
Map<String, Long> relCounts = new HashMap<>();
relCountResult.list().forEach(r ->
relCounts.put(r.get("type").asString(), r.get("count").asLong()));
report.setRelCounts(relCounts);
var totalNodes = session.run("MATCH (n) RETURN count(*) AS total").single().get("total").asLong();
report.setTotalNodes(totalNodes);
var totalRels = session.run("MATCH ()-[rel]->() RETURN count(*) AS total").single().get("total").asLong();
report.setTotalRelationships(totalRels);
}
return report;
}
public void exportOverlapCsv(Path outputPath, List<Map<String, Object>> overlaps) throws IOException {
StringBuilder csv = new StringBuilder();
csv.append("osoba;szpitale;mandaty;afiliacje;confidence;sourceUrls\n");
for (Map<String, Object> row : overlaps) {
String person = (String) row.getOrDefault("person", "");
List<?> szpitale = (List<?>) row.getOrDefault("szpitale", List.of());
List<?> mandaty = (List<?>) row.getOrDefault("mandaty", List.of());
csv.append(person)
.append(";").append(String.join(", ", szpitale.stream().map(Object::toString).toList()))
.append(";").append(String.join(", ", mandaty.stream().map(Object::toString).toList()))
.append("\n");
}
Files.writeString(outputPath, csv.toString());
log.info("Overlap report written to: {}", outputPath);
}
public static class ValidationReport {
public List<Map<String, Object>> hospitalsWithoutDirector = new ArrayList<>();
public List<Map<String, Object>> overlapReport = new ArrayList<>();
public List<Map<String, Object>> multiHospitalReport = new ArrayList<>();
public Map<String, Long> nodeCounts = new HashMap<>();
public Map<String, Long> relCounts = new HashMap<>();
public long totalNodes;
public long totalRelationships;
public void setHospitalsWithoutDirector(List<Map<String, Object>> list) { this.hospitalsWithoutDirector = list; }
public void setOverlapReport(List<Map<String, Object>> list) { this.overlapReport = list; }
public void setMultiHospitalReport(List<Map<String, Object>> list) { this.multiHospitalReport = list; }
public void setNodeCounts(Map<String, Long> map) { this.nodeCounts = map; }
public void setRelCounts(Map<String, Long> map) { this.relCounts = map; }
public void setTotalNodes(long n) { this.totalNodes = n; }
public void setTotalRelationships(long r) { this.totalRelationships = r; }
@Override
public String toString() {
return "Validation report:\n" +
" Total nodes: " + totalNodes + "\n" +
" Total relationships: " + totalRelationships + "\n" +
" Node types: " + nodeCounts + "\n" +
" Relationship types: " + relCounts + "\n" +
" Hospitals without directors: " + hospitalsWithoutDirector.size() + "\n" +
" People with mandates: " + overlapReport.size() + "\n" +
" People in multiple hospitals: " + multiHospitalReport.size() + "\n";
}
}
}
@@ -0,0 +1,54 @@
package com.developx.szpitale.load;
import org.neo4j.driver.Driver;
import java.io.IOException;
import java.nio.file.Path;
public class LoadCommand {
private final Driver neo4jDriver;
public LoadCommand(Driver neo4jDriver) {
this.neo4jDriver = neo4jDriver;
}
public String load(String inDir, String voivodeship, boolean createSchema, boolean wipe, boolean validate) {
try {
if (wipe) {
new GraphSchema(neo4jDriver).dropAll();
System.out.println("Graph wiped.");
}
if (createSchema) {
new GraphSchema(neo4jDriver).createConstraints();
System.out.println("Schema created.");
}
CanonicalReader reader = new CanonicalReader();
var datasets = reader.readAll(Path.of(inDir), voivodeship);
if (datasets.isEmpty()) {
return "Error: No canonical datasets found for voivodeship: " + voivodeship;
}
GraphLoader loader = new GraphLoader(neo4jDriver);
GraphLoader.LoadStats stats = loader.load(datasets);
System.out.println(stats);
if (validate) {
GraphValidator validator = new GraphValidator(neo4jDriver);
GraphValidator.ValidationReport report = validator.validate();
System.out.println(report);
try {
validator.exportOverlapCsv(Path.of("overlap_report.csv"), report.overlapReport);
} catch (IOException e) {
System.err.println("Error writing overlap report: " + e.getMessage());
}
}
return "Load completed successfully.";
} catch (Exception e) {
return "Error during load: " + e.getMessage();
}
}
}
@@ -0,0 +1,17 @@
package com.developx.szpitale.model;
import com.developx.szpitale.model.enums.AffiliationType;
import com.developx.szpitale.model.enums.ConfidenceLevel;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record Affiliation(
@JsonProperty("personId") String personId,
@JsonProperty("type") AffiliationType type,
@JsonProperty("name") String name,
@JsonProperty("confidence") ConfidenceLevel confidence,
@JsonProperty("note") String note,
@JsonProperty("sourceUrls") List<String> sourceUrls
) {
}
@@ -0,0 +1,18 @@
package com.developx.szpitale.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
import java.util.List;
public record CanonicalDataset(
@JsonProperty("schemaVersion") String schemaVersion,
@JsonProperty("voivodeship") String voivodeship,
@JsonProperty("generatedAt") LocalDateTime generatedAt,
@JsonProperty("hospitals") List<Hospital> hospitals,
@JsonProperty("people") List<Person> people,
@JsonProperty("affiliations") List<Affiliation> affiliations,
@JsonProperty("roles") List<Role> roles,
@JsonProperty("mandates") List<Mandate> mandates
) {
}
@@ -0,0 +1,23 @@
package com.developx.szpitale.model;
import com.developx.szpitale.model.enums.LegalForm;
import com.developx.szpitale.model.enums.SupervisoryBodyType;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record Hospital(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
@JsonProperty("shortName") String shortName,
@JsonProperty("city") String city,
@JsonProperty("voivodeship") String voivodeship,
@JsonProperty("legalForm") LegalForm legalForm,
@JsonProperty("foundingBody") String foundingBody,
@JsonProperty("supervisoryBodyType") SupervisoryBodyType supervisoryBodyType,
@JsonProperty("nip") String nip,
@JsonProperty("krs") String krs,
@JsonProperty("website") String website,
@JsonProperty("sourceUrls") List<String> sourceUrls
) {
}
@@ -0,0 +1,16 @@
package com.developx.szpitale.model;
import com.developx.szpitale.model.enums.MandateType;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record Mandate(
@JsonProperty("personId") String personId,
@JsonProperty("mandateType") MandateType mandateType,
@JsonProperty("body") String body,
@JsonProperty("term") String term,
@JsonProperty("voivodeship") String voivodeship,
@JsonProperty("sourceUrls") List<String> sourceUrls
) {
}
@@ -0,0 +1,14 @@
package com.developx.szpitale.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record Person(
@JsonProperty("id") String id,
@JsonProperty("fullName") String fullName,
@JsonProperty("displayName") String displayName,
@JsonProperty("titles") List<String> titles,
@JsonProperty("sourceUrls") List<String> sourceUrls
) {
}
@@ -0,0 +1,41 @@
package com.developx.szpitale.model;
import java.util.List;
import java.util.Map;
public record RawHospitalRecord(
String hospitalName,
String shortName,
String city,
String voivodeship,
String legalForm,
String foundingBody,
String supervisoryBodyType,
String nip,
String krs,
String website,
List<RawPersonEntry> persons
) {
public record RawPersonEntry(
String function,
String fullName,
String displayTitle,
String partyAffiliation,
String confidence,
String sourceUrl,
String organ,
String statusNote
) {
}
public record RawMandate(
String personName,
String mandateType,
String body,
String term,
String voivodeship,
String sourceUrl
) {
}
}
@@ -0,0 +1,25 @@
package com.developx.szpitale.model;
import com.developx.szpitale.model.enums.OrganType;
import com.developx.szpitale.model.enums.RoleStatus;
import com.developx.szpitale.model.enums.RoleType;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDate;
import java.util.List;
public record Role(
@JsonProperty("hospitalId") String hospitalId,
@JsonProperty("personId") String personId,
@JsonProperty("roleType") RoleType roleType,
@JsonProperty("roleLabel") String roleLabel,
@JsonProperty("organ") OrganType organ,
@JsonProperty("status") RoleStatus status,
@JsonProperty("validFrom") LocalDate validFrom,
@JsonProperty("validTo") LocalDate validTo,
@JsonProperty("note") String note,
@JsonProperty("sourceUrls") List<String> sourceUrls
) {
}
@@ -0,0 +1,7 @@
package com.developx.szpitale.model.enums;
public enum AffiliationType {
PARTIA,
KOMITET_WYBORCZY,
KWW_LOKALNY
}
@@ -0,0 +1,8 @@
package com.developx.szpitale.model.enums;
public enum ConfidenceLevel {
POTWIERDZONA,
NIEPOTWIERDZONA,
NIEZWERYFIKOWANA,
BRAK_DANYCH
}
@@ -0,0 +1,8 @@
package com.developx.szpitale.model.enums;
public enum LegalForm {
SPZOZ,
SP_PSYCHIATRYCZNY_ZOZ,
SPOLKA_Z_OO,
INNY
}
@@ -0,0 +1,12 @@
package com.developx.szpitale.model.enums;
public enum MandateType {
RADNY_SEJMIKU,
RADNY_POWIATU,
RADNY_GMINY,
WOJT_BURMISTRZ_PREZYDENT,
STAROSTA,
POSEL,
SENATOR,
MARSZALEK
}
@@ -0,0 +1,7 @@
package com.developx.szpitale.model.enums;
public enum OrganType {
DYREKCJA,
RADA_SPOLECZNA,
RADA_NADZORCZA
}
@@ -0,0 +1,8 @@
package com.developx.szpitale.model.enums;
public enum RoleStatus {
AKTUALNY,
PELNIACY_OBOWIAZKI,
ELEKT,
BYLY
}
@@ -0,0 +1,12 @@
package com.developx.szpitale.model.enums;
public enum RoleType {
DYREKTOR,
ZASTEPCA_DYREKTORA,
GLOWNY_KSIEGOWY,
PRZELOZONA_PIELEGNIARKOW,
CZLONEK_ORGANU_NADZORCZEGO,
PRZEWODNICZACY_ORGANU,
SEKRETARZ_ORGANU,
Z_CA_PRZEWODNICZACEGO
}
@@ -0,0 +1,6 @@
package com.developx.szpitale.model.enums;
public enum SupervisoryBodyType {
RADA_SPOLECZNA,
RADA_NADZORCZA
}
@@ -0,0 +1,14 @@
spring:
neo4j:
uri: bolt://localhost:7687
authentication:
username: neo4j
password: password
application:
name: szpitale-graph
app:
ingest:
canonical-dir: canonical
neo4j:
batch-size: 1000
@@ -0,0 +1,68 @@
package com.developx.szpitale.ingest.normalize;
import com.developx.szpitale.model.Person;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class DeduplicatorTest {
private Deduplicator deduplicator;
@BeforeEach
void setUp() {
deduplicator = new Deduplicator();
}
@Test
void checkFuzzyDuplicates_similarNames_reportedNotMerged() {
Person janKowalski = new Person(
"person:jan-kowalski",
"Jan Kowalski",
"Jan Kowalski",
List.of(),
List.of("https://example.com/1")
);
Person janKowaski = new Person(
"person:jan-kowaski",
"Jan Kowaski",
"Jan Kowaski",
List.of(),
List.of("https://example.com/2")
);
List<Person> input = List.of(janKowalski, janKowaski);
List<Person> result = deduplicator.deduplicate(input);
assertEquals(2, result.size(), "Should not merge two similar names - both should remain as separate entries");
assertTrue(result.stream().anyMatch(p -> p.id().equals("person:jan-kowalski")));
assertTrue(result.stream().anyMatch(p -> p.id().equals("person:jan-kowaski")));
}
@Test
void deduplicate_exactSlugMatches_merged() {
Person first = new Person(
"person:jan-kowalski",
"Jan Kowalski",
"Jan Kowalski",
List.of(),
List.of("https://example.com/1")
);
Person second = new Person(
"person:jan-kowalski",
"Jan Kowalski",
"Jan Kowalski",
List.of(),
List.of("https://example.com/2")
);
List<Person> result = deduplicator.deduplicate(List.of(first, second));
assertEquals(1, result.size(), "Should merge persons with same slug");
assertEquals(2, result.get(0).sourceUrls().size(), "Merged person should have all source URLs");
}
}
@@ -0,0 +1,14 @@
spring:
neo4j:
uri: bolt://localhost:7687
authentication:
username: neo4j
password: password
application:
name: szpitale-graph
app:
ingest:
canonical-dir: canonical
neo4j:
batch-size: 1000

Some files were not shown because too many files have changed in this diff Show More