T1: Deduplicator compile fix + unit tests [checkpoint]
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Generated
+10
@@ -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/
|
||||
Generated
+18
@@ -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>
|
||||
Generated
+6
@@ -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>
|
||||
Generated
+20
@@ -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>
|
||||
Generated
+14
@@ -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>
|
||||
@@ -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.
|
||||
@@ -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`).
|
||||
@@ -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 ~5–10 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.2–5.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.2–13.3.
|
||||
|
||||
---
|
||||
|
||||
**Koniec planu**
|
||||
@@ -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
|
||||
@@ -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
@@ -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/",""
|
||||
|
@@ -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 (V–VI 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 (PSL–Polska 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 PO–PSL); 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.*
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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": "" }
|
||||
]
|
||||
}
|
||||
+295
@@ -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 "$@"
|
||||
Vendored
+189
@@ -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"
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -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();
|
||||
}
|
||||
}
|
||||
+64
@@ -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;
|
||||
}
|
||||
}
|
||||
+361
@@ -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;
|
||||
}
|
||||
}
|
||||
+10
@@ -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
|
||||
}
|
||||
+6
@@ -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
|
||||
+68
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user