Compare commits
36
Commits
master
..
6a68406923
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a68406923 | ||
|
|
7426ee8383 | ||
|
|
437bb28e69 | ||
|
|
a92028e33b | ||
|
|
fdb212fe4c | ||
|
|
055b0fcd08 | ||
|
|
5155c76d02 | ||
|
|
b5a4f642d9 | ||
|
|
81d75da7ce | ||
|
|
bb7b8453e2 | ||
|
|
46529e4b88 | ||
|
|
e9dd17b291 | ||
|
|
779c447670 | ||
|
|
98ddfce42d | ||
|
|
72cd188d52 | ||
|
|
066e22fb1d | ||
|
|
09c291a14c | ||
|
|
38e0350f18 | ||
|
|
013cc823b6 | ||
|
|
5dcafa4ef0 | ||
|
|
36b879ae95 | ||
|
|
366444969a | ||
|
|
39abbd0be3 | ||
|
|
5ae9dbf2a6 | ||
|
|
10377e95e2 | ||
|
|
92b47e006e | ||
|
|
11a51a8d79 | ||
|
|
5eaf859274 | ||
|
|
a34ef20fd4 | ||
|
|
620a6622fd | ||
|
|
2d20e5739a | ||
|
|
f0348451af | ||
|
|
6796214afe | ||
|
|
302f2733d3 | ||
|
|
906821b296 | ||
|
|
87141ecfb0 |
@@ -1,61 +0,0 @@
|
||||
# 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
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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
|
||||
@@ -1,65 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,163 +0,0 @@
|
||||
<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%)
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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`.
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,9 +0,0 @@
|
||||
"""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"
|
||||
@@ -1,3 +0,0 @@
|
||||
from .cli import main
|
||||
|
||||
main()
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,342 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,139 +0,0 @@
|
||||
#!/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}")
|
||||
@@ -1,213 +0,0 @@
|
||||
#!/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}")
|
||||
@@ -1,38 +0,0 @@
|
||||
# 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
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,30 +0,0 @@
|
||||
# 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
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,48 +0,0 @@
|
||||
# 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
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
/.idea/
|
||||
Generated
-10
@@ -1,10 +0,0 @@
|
||||
# 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
@@ -1,18 +0,0 @@
|
||||
<?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
@@ -1,6 +0,0 @@
|
||||
<?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
@@ -1,20 +0,0 @@
|
||||
<?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
@@ -1,14 +0,0 @@
|
||||
<?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>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,89 +0,0 @@
|
||||
# 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 PLAN.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 PLAN.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
|
||||
|
||||
PLAN.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** (PLAN.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.
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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 [PLAN.md](PLAN.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`).
|
||||
@@ -1,810 +0,0 @@
|
||||
# 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`.
|
||||
- `PLAN2.md` (delta) wchłonięty tutaj: rozszerzenie `Person` o `title`/`pwzNumber` (→ 2, 4.7), nowe encje `Organization`/`Company` z powiązaniami po KRS/NIP/CEIDG (→ 2, 4.8, 5.2), nowe zadania kolejki agenta (→ T9–T13). Plik `PLAN2.md` usunięty po scaleniu.
|
||||
- Kolejka §11 przepisana pod konkretny model wykonawczy: **Qwen-3.6-35B-A3B** (MoE, ~3B aktywnych parametrów na token, mały kontekst efektywny). Reguły atomowości zadań i zakaz planowania (już obecne w poprzedniej wersji dla „małego lokalnego modelu") pozostają — dopisano jawne odniesienie do tego modelu, by przyszłe zadania kalibrować pod jego ograniczenia kontekstu i skłonność do zmyślania API.
|
||||
|
||||
---
|
||||
|
||||
## Proces pracy: TDD (obowiązkowy)
|
||||
|
||||
Cały kod tego planu powstaje test-first. Agent **nie pisze kodu produkcyjnego zanim nie istnieje test, który go wymaga**. Pełne reguły w [CLAUDE.md](CLAUDE.md); tu skrót obowiązujący dla każdego zadania (§11) i każdej klasy z sekcji 3.
|
||||
|
||||
**Cykl red → green → refactor (małe kroki, jeden zachowaniowy przyrost = jeden obieg):**
|
||||
1. **RED** — napisz test opisujący pożądane zachowanie w `src/test/java/...` (mirror pakietu z `src/main/java`). Uruchom `./mvnw -q test -Dtest=NazwaTestu` i zobacz, że **failuje** (kompiluje się, asercja nie przechodzi). Test bez uprzedniej porażki nic nie dowodzi.
|
||||
2. **GREEN** — napisz **minimalny** kod produkcyjny zazieleniający test. Bez funkcji, których żaden test nie wymaga.
|
||||
3. **REFACTOR** — posprzątaj kod i test przy zielonym pasku; testy nadal zielone.
|
||||
|
||||
**Zasady wiążące:**
|
||||
- **Test najpierw.** Bug fix zaczyna się od testu reprodukującego buga (RED), potem poprawka (np. T1 `Deduplicator`).
|
||||
- **Dług techniczny:** istniejący kod produkcyjny powstał bez testów. Dotykając klasy bez pokrycia — najpierw dopisz **charakteryzujący** test aktualnego zachowania (green), potem prowadź zmianę cyklem TDD (patrz T2–T3).
|
||||
- **Testy jednostkowe** (bez Neo4j, szybkie, deterministyczne): normalizacja (`NameNormalizer`, `PartyNormalizer`), deduplikacja (`Deduplicator`), parsowanie źródeł (`MarkdownRegistrySource`), serializacja canonical (`CanonicalWriter`/`CanonicalReader`).
|
||||
- **Testy integracyjne** dla warstwy `load`: Neo4j przez **Testcontainers** (`org.testcontainers:neo4j`) — idempotencja `MERGE`, constrainty schematu, raporty walidatora (tag **[NEO4J]**, wymaga Dockera).
|
||||
- **Nazwy testów** opisują zachowanie: `metoda_warunek_oczekiwanyWynik` (np. `normalize_polishDiacritics_strippedToAsciiSlug`).
|
||||
- **Definicja ukończenia:** zielony `./mvnw -q test` jest warunkiem uznania zadania za zrobione. Jeśli testy nie mogą się uruchomić (brak `mvn`/Dockera) — zgłoś jawnie, nie deklaruj sukcesu (tag `[~]` + notatka).
|
||||
|
||||
---
|
||||
|
||||
## 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."],
|
||||
"title": "prof. dr hab. n. med.", // tytuł skondensowany (jedno pole, do wyświetlania obok fullName)
|
||||
"pwzNumber": "1234567", // Prawo Wykonywania Zawodu, z rejestr.nil.org.pl; null gdy nieznaleziony
|
||||
"sourceUrls": ["https://uskwb.pl/dyrekcja-szpitala/"]
|
||||
}
|
||||
],
|
||||
"organizations": [ // NGO/fundacje/stowarzyszenia powiązane z osobami przez KRS
|
||||
{
|
||||
"id": "org:fundacja-xyz",
|
||||
"name": "Fundacja XYZ",
|
||||
"krs": "0000123456",
|
||||
"nip": null,
|
||||
"sourceUrls": ["https://ems.ms.gov.pl/..."]
|
||||
}
|
||||
],
|
||||
"companies": [ // spółki/działalności gospodarcze powiązane z osobami
|
||||
{
|
||||
"id": "company:abc-sp-zoo",
|
||||
"name": "ABC sp. z o.o.",
|
||||
"krs": "0000654321",
|
||||
"nip": "1234567890",
|
||||
"ceidgId": null,
|
||||
"sourceUrls": ["https://wyszukiwarka-krs.ms.gov.pl/..."]
|
||||
}
|
||||
],
|
||||
"personOrganizationLinks": [ // osoba ↔ organizacja, dopasowanie po KRS
|
||||
{
|
||||
"personId": "person:jan-kochanowicz",
|
||||
"organizationId": "org:fundacja-xyz",
|
||||
"roleLabel": "Prezes Zarządu",
|
||||
"basis": "KRS", // klucz, po którym dopasowano powiązanie
|
||||
"sourceUrls": ["https://ems.ms.gov.pl/..."]
|
||||
}
|
||||
],
|
||||
"organizationLinks": [ // organizacja ↔ organizacja, po KRS/NIP (np. wspólny adres/zarząd)
|
||||
{
|
||||
"fromOrganizationId": "org:fundacja-xyz",
|
||||
"toOrganizationId": "org:inna-fundacja",
|
||||
"basis": "NIP", // KRS | NIP
|
||||
"note": "wspólny adres siedziby",
|
||||
"sourceUrls": ["https://ems.ms.gov.pl/..."]
|
||||
}
|
||||
],
|
||||
"personCompanyLinks": [ // osoba ↔ firma, po KRS/CEIDG/NIP
|
||||
{
|
||||
"personId": "person:jan-kochanowicz",
|
||||
"companyId": "company:abc-sp-zoo",
|
||||
"roleLabel": "Wspólnik",
|
||||
"basis": "KRS", // KRS | CEIDG | NIP
|
||||
"sourceUrls": ["https://wyszukiwarka-krs.ms.gov.pl/..."]
|
||||
}
|
||||
],
|
||||
"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. Nowy zamknięty enum `LinkBasis` (`KRS`, `NIP`, `CEIDG`) opisuje klucz dopasowania powiązań organizacja/firma.
|
||||
- Pola `note` i `confidence` przenoszą metadane jakościowe z materiału roboczego (kluczowe dla kontekstu dziennikarskiego).
|
||||
- `id` organizacji/firmy jest slugiem z nazwy + (gdy dostępny) KRS w nawiasie logiki dedupu — dwie różne organizacje o tej samej nazwie, ale różnym KRS, **nie są scalane** (analogicznie do zasady dla `Person` w 4.3 pkt 4).
|
||||
- `pwzNumber` bywa `null` — brak trafienia w rejestr.nil.org.pl to nie błąd, tylko luka odnotowywana w `ingest-report.json` (patrz 4.7).
|
||||
|
||||
---
|
||||
|
||||
## 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` |
|
||||
| Rejestry zewnętrzne | rejestr.nil.org.pl (PWZ, formularz HTML — brak API), wyszukiwarka-krs.ms.gov.pl / api-krs (KRS), CEIDG API (dane.biznes.gov.pl) |
|
||||
|
||||
### 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
|
||||
│ │ ├── Organization.java Company.java PersonOrganizationLink.java OrganizationLink.java PersonCompanyLink.java
|
||||
│ │ └── enums/ (LegalForm, RoleType, MandateType, Confidence, LinkBasis, ...)
|
||||
│ ├── 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
|
||||
│ │ │ ├── PwzRegistrySource.java # uzupełnienie pwzNumber z rejestr.nil.org.pl (4.7)
|
||||
│ │ │ ├── KrsSource.java # organizacje/firmy + powiązania po KRS (4.8)
|
||||
│ │ │ └── CeidgSource.java # powiązania osoba↔firma po CEIDG/NIP (4.8)
|
||||
│ │ ├── 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`).
|
||||
- **`PwzRegistrySource`** — uzupełnia `pwzNumber` osoby z `rejestr.nil.org.pl` (formularz imię+nazwisko, patrz 4.7).
|
||||
- **`KrsSource`** — wyszukuje organizacje/firmy powiązane z osobą po KRS (patrz 4.8).
|
||||
- **`CeidgSource`** — kontrola krzyżowa działalności gospodarczych osoby po CEIDG/NIP (patrz 4.8).
|
||||
|
||||
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.7. Uzupełnienie numeru PWZ — `PwzRegistrySource`
|
||||
|
||||
`rejestr.nil.org.pl` (Naczelna Izba Lekarska) udostępnia wyszukiwarkę lekarzy **wyłącznie jako formularz HTML** (imię + nazwisko), bez publicznego API. Wzbogacenie działa jako osobny krok w `ingest`, **po** normalizacji osób (potrzebuje `fullName`):
|
||||
|
||||
1. Dla każdej osoby o `titles` sugerujących zawód medyczny (`dr`, `dr n. med.`, `prof.`, `lek.`, itp. — heurystyka w `PwzRegistrySource`, nie każda osoba w radzie jest lekarzem) wyślij zapytanie z `fullName`.
|
||||
2. **Wiele trafień** (częste nazwiska) → `pwzNumber = null`, `note = "wieloznaczne trafienie NIL, wymaga ręcznej weryfikacji"`, wpis w `ingest-report.json`.
|
||||
3. **Brak trafienia** → `pwzNumber = null`, brak `note` (osoba może nie być lekarzem — to nie błąd).
|
||||
4. Rate-limiting jak dla `HtmlScraperSource` (4.2/7) — to ten sam serwis żywy, nie plik `.md`.
|
||||
|
||||
**Granica odpowiedzialności:** to wzbogacenie jest **deterministyczne i automatyczne** (1:1 zapytanie→wynik po nazwisku), inaczej niż `deep-research` (4.6) — nie wymaga oceny dziennikarskiej, tylko technicznej weryfikacji przy wieloznaczności.
|
||||
|
||||
### 4.8. Organizacje i firmy — `KrsSource` / `CeidgSource`
|
||||
|
||||
Rozszerzenie modelu o encje `Organization` (fundacje, stowarzyszenia) i `Company` (spółki, działalności gospodarcze) powiązane z osobami z organów szpitali — cel: ujawnić potencjalne konflikty interesów.
|
||||
|
||||
1. **`KrsSource`** — dla każdej osoby: wyszukanie w KRS (`wyszukiwarka-krs.ms.gov.pl` / `api-krs`) wpisów, gdzie figuruje jako zarząd/wspólnik/fundator. Zwraca `Organization`/`Company` + `personOrganizationLinks`/`personCompanyLinks` z `basis=KRS`.
|
||||
2. Powiązania **organizacja↔organizacja** (`organizationLinks`) — wspólny KRS-owy adres, zarząd lub `nip` między dwiema organizacjami; `basis=KRS` lub `basis=NIP`.
|
||||
3. **`CeidgSource`** — kontrola krzyżowa jednoosobowych działalności gospodarczych osoby po CEIDG (`dane.biznes.gov.pl`, API publiczne) i po `nip`; `basis=CEIDG` lub `basis=NIP`.
|
||||
4. Deduplikacja: `Organization`/`Company` scalane po `krs` (klucz twardy, jednoznaczny) — w przeciwieństwie do `Person`, gdzie deduplikacja jest fuzzy i ostrożna (4.3 pkt 4), bo KRS/NIP są identyfikatorami unikalnymi z rejestru państwowego.
|
||||
5. Brak dopasowania po KRS/CEIDG/NIP dla danej osoby = brak wpisu (nie każda osoba w radzie ma powiązania biznesowe) — nie jest to luka do raportowania.
|
||||
|
||||
### 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, title, pwzNumber, 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)
|
||||
- `:Organization {id, name, krs, nip, sourceUrls}` (fundacje, stowarzyszenia)
|
||||
- `:Company {id, name, krs, nip, ceidgId, sourceUrls}` (spółki, działalności gospodarcze)
|
||||
|
||||
**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 | — |
|
||||
| `[:CZLONEK_ORGANIZACJI]` | Person → Organization | `roleLabel, basis (KRS), sourceUrls` |
|
||||
| `[:POWIAZANA_Z]` | Organization → Organization | `basis (KRS/NIP), note, sourceUrls` |
|
||||
| `[:POWIAZANY_Z_FIRMA]` | Person → Company | `roleLabel, basis (KRS/CEIDG/NIP), sourceUrls` |
|
||||
|
||||
> 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 CONSTRAINT org_id IF NOT EXISTS FOR (o:Organization) REQUIRE o.id IS UNIQUE;
|
||||
CREATE CONSTRAINT company_id IF NOT EXISTS FOR (c:Company) REQUIRE c.id IS UNIQUE;
|
||||
CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.fullName);
|
||||
CREATE INDEX person_pwz IF NOT EXISTS FOR (p:Person) ON (p.pwzNumber);
|
||||
```
|
||||
|
||||
### 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, title:p.title, pwzNumber:p.pwzNumber, sourceUrls:p.sourceUrls};
|
||||
|
||||
UNWIND $organizations AS o
|
||||
MERGE (org:Organization {id: o.id})
|
||||
SET org += {name:o.name, krs:o.krs, nip:o.nip, sourceUrls:o.sourceUrls};
|
||||
|
||||
UNWIND $companies AS c
|
||||
MERGE (comp:Company {id: c.id})
|
||||
SET comp += {name:c.name, krs:c.krs, nip:c.nip, ceidgId:c.ceidgId, sourceUrls:c.sourceUrls};
|
||||
|
||||
UNWIND $personOrganizationLinks AS pol
|
||||
MATCH (p:Person {id:pol.personId}), (org:Organization {id:pol.organizationId})
|
||||
MERGE (p)-[m:CZLONEK_ORGANIZACJI]->(org)
|
||||
SET m += {roleLabel:pol.roleLabel, basis:pol.basis, sourceUrls:pol.sourceUrls};
|
||||
|
||||
UNWIND $organizationLinks AS ol
|
||||
MATCH (a:Organization {id:ol.fromOrganizationId}), (b:Organization {id:ol.toOrganizationId})
|
||||
MERGE (a)-[m:POWIAZANA_Z]->(b)
|
||||
SET m += {basis:ol.basis, note:ol.note, sourceUrls:ol.sourceUrls};
|
||||
|
||||
UNWIND $personCompanyLinks AS pcl
|
||||
MATCH (p:Person {id:pcl.personId}), (comp:Company {id:pcl.companyId})
|
||||
MERGE (p)-[m:POWIAZANY_Z_FIRMA]->(comp)
|
||||
SET m += {roleLabel:pcl.roleLabel, basis:pcl.basis, sourceUrls:pcl.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;
|
||||
|
||||
// Osoby w organie szpitala mające jednocześnie powiązanie biznesowe (konflikt interesów)
|
||||
MATCH (h:Hospital)-[:CZLONEK_ORGANU]->(p:Person)
|
||||
WHERE (p)-[:POWIAZANY_Z_FIRMA]->() OR (p)-[:CZLONEK_ORGANIZACJI]->()
|
||||
OPTIONAL MATCH (p)-[:POWIAZANY_Z_FIRMA]->(comp:Company)
|
||||
OPTIONAL MATCH (p)-[:CZLONEK_ORGANIZACJI]->(org:Organization)
|
||||
RETURN p.fullName, h.name, collect(DISTINCT comp.name) AS firmy, collect(DISTINCT org.name) AS organizacje;
|
||||
```
|
||||
Eksport `overlap_report.csv` (osoba, szpitale, funkcje, partia, mandaty, firmy/organizacje, `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 |
|
||||
| Wieloznaczne trafienie w rejestr.nil.org.pl (popularne nazwisko) | `pwzNumber=null`, `note="wieloznaczne trafienie NIL, wymaga ręcznej weryfikacji"`, wpis w `ingest-report.json` |
|
||||
| Brak trafienia w KRS/CEIDG dla osoby | Brak wpisu w `personOrganizationLinks`/`personCompanyLinks` — to nie luka (nie każdy ma powiązania biznesowe) |
|
||||
| Ta sama nazwa organizacji, różny KRS | Osobne węzły `:Organization` (dedup po `krs`, nie po nazwie) |
|
||||
|
||||
---
|
||||
|
||||
## 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/firmami/organizacjami, z `sourceUrls` i `confidence`.
|
||||
5. **`ingest-report.json`** — statystyki i rejestr luk/rozbieżności per województwo (w tym wieloznaczne trafienia PWZ).
|
||||
6. **Testy** — JUnit + Testcontainers (Neo4j) dla warstwy ładowania; testy jednostkowe normalizacji/deduplikacji.
|
||||
7. **Encje `Organization`/`Company`** w modelu kanonicznym i grafie, z powiązaniami do osób po KRS/CEIDG/NIP (sekcja 4.8, 5.2).
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
- [ ] `Person` rozszerzony o `title`/`pwzNumber` (sekcja 2, 4.7) + `PwzRegistrySource`.
|
||||
- [ ] Nowe rekordy modelu: `Organization`, `Company`, `PersonOrganizationLink`, `OrganizationLink`, `PersonCompanyLink` (sekcja 2) + `KrsSource`/`CeidgSource` (sekcja 4.8).
|
||||
- [ ] Warstwa 2: węzły `:Organization`/`:Company` + relacje `CZLONEK_ORGANIZACJI`/`POWIAZANA_Z`/`POWIAZANY_Z_FIRMA` (sekcja 5.2, 5.4) + constrainty (5.3).
|
||||
|
||||
---
|
||||
|
||||
## 11. Kolejka zadań dla lokalnego agenta (TDD, kroki atomowe)
|
||||
|
||||
Ta sekcja jest pisana pod lokalny model wykonawczy **Qwen-3.6-35B-A3B** (MoE, ~3B aktywnych parametrów na token, mały efektywny kontekst, skłonność do zmyślania nieistniejących API przy niedoprecyzowanym zadaniu). 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. Zasada 7 (poniżej) jest przy tym modelu szczególnie krytyczna: A3B chętnie "dopisuje" wygodne metody, których nie ma w `src/main/java` — zawsze czytaj plik przed wywołaniem metody.
|
||||
|
||||
### 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).
|
||||
|
||||
**T9 — Rozszerzyć `Person` o `title` i `pwzNumber`**
|
||||
- Plik: `src/main/java/com/developx/szpitale/model/Person.java` (rekord z 5 polami: `id, fullName, displayName, titles, sourceUrls`).
|
||||
- Zamiar: dodać dwa nowe pola rekordu: `@JsonProperty("title") String title` (może być `null`) i `@JsonProperty("pwzNumber") String pwzNumber` (może być `null`), jako **ostatnie** dwa parametry konstruktora rekordu.
|
||||
- TDD:
|
||||
1. Test `PersonTest.person_withTitleAndPwzNumber_fieldsAccessible` w `src/test/java/com/developx/szpitale/model/PersonTest.java`: zbuduj `Person` z niepustym `title` i `pwzNumber`, asercja że `person.title()` i `person.pwzNumber()` zwracają wpisane wartości. RED (kompilacja nie przejdzie, bo konstruktor ma 5 argumentów) → dodaj pola → GREEN.
|
||||
2. Znajdź **wszystkie** miejsca konstruujące `new Person(...)` (`grep -rn "new Person(" src/`) i dopisz `null, null` (lub realną wartość jeśli już znana) na końcu wywołania — inaczej build nie skompiluje się.
|
||||
- DoD: `./mvnw -q compile` przechodzi; `./mvnw -q test -Dtest=PersonTest` zielony.
|
||||
|
||||
**T10 — `PwzRegistrySource` (fixture, bez żywego HTTP w teście)**
|
||||
- Nowy plik: `src/main/java/com/developx/szpitale/ingest/source/PwzRegistrySource.java`.
|
||||
- Zamiar (sekcja 4.7): metoda `Optional<String> lookupPwz(String fullName)` przyjmuje wynik zapytania do `rejestr.nil.org.pl` jako **wstrzykniętą zależność** (np. interfejs `PwzLookupClient` z metodą `List<String> query(String fullName)`), żeby test nie robił żywego HTTP.
|
||||
- Fixture: `src/test/resources/fixtures/nil-single-match.json` z jednym wynikiem (`["1234567"]`) i `nil-multiple-matches.json` z dwoma.
|
||||
- TDD:
|
||||
1. Test `PwzRegistrySourceTest.lookupPwz_singleMatch_returnsPwzNumber` → mock `PwzLookupClient` zwraca jeden wynik → `lookupPwz` zwraca `Optional.of("1234567")`.
|
||||
2. Test `PwzRegistrySourceTest.lookupPwz_multipleMatches_returnsEmpty` → mock zwraca 2 wyniki → `lookupPwz` zwraca `Optional.empty()` (wieloznaczność, patrz 4.7 pkt 2 — **nie zgadywać**, który wynik jest właściwy).
|
||||
3. Test `PwzRegistrySourceTest.lookupPwz_noMatch_returnsEmpty` → mock zwraca pustą listę → `Optional.empty()`.
|
||||
- DoD: `./mvnw -q test -Dtest=PwzRegistrySourceTest` zielony. Nie implementuj żywego klienta HTTP w tym zadaniu — tylko interfejs `PwzLookupClient` + logika `PwzRegistrySource` na nim.
|
||||
|
||||
**T11 — Rekordy `Organization`/`Company` + linki (round-trip)**
|
||||
- Nowe pliki: `src/main/java/com/developx/szpitale/model/Organization.java`, `Company.java`, `PersonOrganizationLink.java`, `OrganizationLink.java`, `PersonCompanyLink.java` — pola dokładnie jak w sekcji 2 PLAN.md (przykłady JSON), jako rekordy Java z `@JsonProperty` (wzoruj się na `Hospital.java`/`Person.java`).
|
||||
- Zamiar: `CanonicalDataset` (`src/main/java/com/developx/szpitale/model/CanonicalDataset.java`) dostaje 5 nowych pól-list: `organizations, companies, personOrganizationLinks, organizationLinks, personCompanyLinks` (domyślnie puste listy, nie `null`).
|
||||
- TDD:
|
||||
1. Test `OrganizationCompanyRoundTripTest.write_thenRead_preservesOrganizationsAndCompanies` w `src/test/java/com/developx/szpitale/ingest/` (mirror `CanonicalRoundTripTest` z T5): zbuduj `CanonicalDataset` z 1 `Organization`, 1 `Company`, po 1 linku każdego typu; zapisz `CanonicalWriter`, odczytaj `CanonicalReader`, porównaj pola. RED najpierw.
|
||||
2. Minimalny kod: dodaj rekordy + pola w `CanonicalDataset`, sprawdź że Jackson serializuje/deserializuje bez adnotacji dodatkowych (jak reszta modelu).
|
||||
- DoD: `./mvnw -q test -Dtest=OrganizationCompanyRoundTripTest` zielony.
|
||||
|
||||
**T12 — [NEO4J] `GraphLoader` ładuje `:Organization`/`:Company`**
|
||||
- Plik prod: `src/main/java/com/developx/szpitale/load/GraphLoader.java` (przeczytaj istniejącą metodę ładującą `Hospital`/`Person`, wzoruj się na jej strukturze).
|
||||
- Zamiar (sekcja 5.4): dodać metodę analogiczną do istniejącego ładowania węzłów, wykonującą `UNWIND $organizations ... MERGE (:Organization {id})`, `UNWIND $companies ... MERGE (:Company {id})` oraz 3 `MERGE` relacji: `CZLONEK_ORGANIZACJI`, `POWIAZANA_Z`, `POWIAZANY_Z_FIRMA` (dokładne zapytania Cypher w sekcji 5.4 PLAN.md).
|
||||
- Test integracyjny: `GraphLoaderOrganizationIT` z `@Testcontainers` (wzoruj się na istniejącym wzorcu z T6, jeśli `GraphLoaderIT` już istnieje — skopiuj setup kontenera).
|
||||
- `load_organizationAndCompany_nodesCreatedWithLinks` → załaduj 1 osobę + 1 organizację + 1 firmę + odpowiednie linki, sprawdź że istnieją węzły `:Organization`, `:Company` i relacje do `:Person`.
|
||||
- Wymaga Dockera. Bez Dockera → `[~]` + notatka (jak T6/T7).
|
||||
- DoD: `./mvnw -q test -Dtest=GraphLoaderOrganizationIT` zielony (lub pominięte z notatką).
|
||||
|
||||
**T13 — [NEO4J] `GraphValidator` — zapytanie o konflikt interesów**
|
||||
- Plik prod: `src/main/java/com/developx/szpitale/load/GraphValidator.java`.
|
||||
- Zamiar (sekcja 5.6): dodać metodę zwracającą wynik zapytania „osoby w organie szpitala z powiązaniem biznesowym" (dokładny Cypher w sekcji 5.6 PLAN.md, blok „konflikt interesów").
|
||||
- Test integracyjny `GraphValidatorOrganizationIT`: wgraj dataset z 1 osobą mającą `CZLONEK_ORGANU` (szpital) i `POWIAZANY_Z_FIRMA` (firma); asercja: wynik zawiera tę osobę z nazwą firmy.
|
||||
- Wymaga Dockera. Bez Dockera → `[~]` + notatka.
|
||||
- DoD: `./mvnw -q test -Dtest=GraphValidatorOrganizationIT` zielony (lub pominięte z notatką).
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
- [x] T1 Deduplicator compile fix
|
||||
- [x] T2 NameNormalizer testy
|
||||
- [x] T3 PartyNormalizer testy
|
||||
- [x] T4 MarkdownRegistrySource test
|
||||
- [x] T5 Canonical round-trip
|
||||
- [~] T6 GraphLoader idempotencja [NEO4J] (skipped: docker-java 3.4.0 in testcontainers 1.20.x hardcodes API v1.32, Daemon requires v1.44+)
|
||||
- [~] T7 GraphValidator + CSV [NEO4J] (skipped: same docker-api issue as T6)
|
||||
- [x] T8 Zielony pełny build
|
||||
- [x] T9 Person: title/pwzNumber
|
||||
- [x] T10 PwzRegistrySource
|
||||
- [x] T11 Organization/Company rekordy + round-trip
|
||||
- [~] T12 GraphLoader: Organization/Company [NEO4J] (skipped: Docker API version mismatch - testcontainers docker-java client v1.32, min 1.44)
|
||||
- [~] T13 GraphValidator: konflikt interesow [NEO4J] (skipped: Docker API too old — same cause as T6)
|
||||
|
||||
**Dziennik (data — zadanie — wynik):**
|
||||
- 2026-07-07 — T1 — Deduplicator compile fix + unit tests [checkpoint] (commit 38a93d0)
|
||||
- 2026-07-07 — T2 — NameNormalizer testy + transliteratePolish, token-strip titles [checkpoint] (commit afe107c)
|
||||
- 2026-07-07 — T3 — PartyNormalizer testy + KO alias [checkpoint] (commit bc4eaac)
|
||||
- 2026-07-07 — T4 — MarkdownRegistrySource test + fix isPersonTable check [checkpoint] (commit 7f636a1)
|
||||
- 2026-07-07 — T5 — CanonicalWriter/Reader round-trip test [checkpoint] (commit b072cb3)
|
||||
- 2026-07-07 — T6/T8 — skip NEO4J tests (Docker API zbyt stary) + pełny build zielony [checkpoint] (commit 59d27b1)
|
||||
- 2026-07-08 — PLAN.md scalony z PLAN2.md (Person.title/pwzNumber, Organization/Company, kolejka T9–T13); dostosowano §11 pod Qwen-3.6-35B-A3B.
|
||||
- 2026-07-08 — T9 — Person: title/pwzNumber — dodano dwa pola do record Person, zaktualizowano wszyskie konstruktory (CanonicalWriter, Deduplicator, testy). 7 testow zielonych.
|
||||
- 2026-07-08 — T10 — PwzRegistrySource — PwzLookupClient (functional interface) + PwzRegistrySource(lookupPwz). 4 testy zielone.
|
||||
- 2026-07-08 — T11 — Organization/Company entites + round-trip — Organization, Company, PersonOrganizationLink, OrganizationLink, PersonCompanyLink + LinkBasis enum. CanonicalDataset + CanonicalReader zaktualizowane.
|
||||
- 2026-07-08 — T12 — GraphLoader Organization/Company loading (prod code committed), GraphLoaderOrganizationIT (skipped: Docker API too old, same as T6/T7)
|
||||
|
||||
---
|
||||
|
||||
## 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**
|
||||
@@ -1,78 +1,107 @@
|
||||
# szpitale-graph
|
||||
# Deep Research Skill for Claude Code / OpenCode
|
||||
|
||||
Graph DB project. Polish hospitals data in Neo4j.
|
||||
[English](README.md) | [中文](README.zh.md)
|
||||
|
||||
Two-layer architecture. Ingest sources → canonical JSON → load into Neo4j.
|
||||
> Inspired by [RhinoInsight: Improving Deep Research through Control Mechanisms for Model Behavior and Context](https://arxiv.org/abs/2511.18743)
|
||||
|
||||
## Stack
|
||||
A structured research workflow skill for Claude Code, supporting two-phase research: outline generation (extensible) and deep investigation. Human-in-the-loop design ensures precise control at every stage.
|
||||
|
||||
- Java 21
|
||||
- Maven (use `./mvnw` wrapper)
|
||||
- Spring Boot 3.4.4 + Spring Shell
|
||||
- Neo4j 5
|
||||
- JUnit 5 + Testcontainers
|
||||
## Use Cases
|
||||
|
||||
## Key commands
|
||||
- **Academic Research**: Paper surveys, benchmark reviews, literature analysis
|
||||
- **Technical Research**: Technology comparison, framework evaluation, tool selection
|
||||
- **Market Research**: Competitor analysis, industry trends, product comparison
|
||||
- **Due Diligence**: Company research, investment analysis, risk assessment
|
||||
|
||||
From `szpitale-graph/`:
|
||||
## Installation
|
||||
|
||||
### Claude Code
|
||||
```bash
|
||||
./mvnw -q compile # compile
|
||||
./mvnw -q test # all tests
|
||||
./mvnw -q test -Dtest=ClassName # single test
|
||||
./mvnw -q verify # full verify
|
||||
java -jar target/szpitale-graph.jar ingest --voivodeship <voiv> # layer 1
|
||||
java -jar target/szpitale-graph.jar load --voivodeship <voiv> # layer 2
|
||||
# English version
|
||||
cp -r skills/research-en/* ~/.claude/skills/
|
||||
|
||||
# Chinese version
|
||||
cp -r skills/research-zh/* ~/.claude/skills/
|
||||
|
||||
# Required: Install agent
|
||||
cp agents/web-search-agent.md ~/.claude/agents/
|
||||
|
||||
# Required: Install Python dependency
|
||||
pip install pyyaml
|
||||
```
|
||||
|
||||
Neo4j:
|
||||
|
||||
### OpenCode (default: gpt-5.2)
|
||||
```bash
|
||||
docker compose up -d # start Neo4j (UI :7474, Bolt :7687)
|
||||
docker compose down -v # stop + remove volumes
|
||||
# Skills (same as Claude Code)
|
||||
cp -r skills/research-en/* ~/.claude/skills/ # or research-zh for Chinese
|
||||
|
||||
# Required: Install agent
|
||||
cp agents/web-search-opencode.md ~/.config/opencode/agent/web-search.md
|
||||
|
||||
# Required: Install Python dependency
|
||||
pip install pyyaml
|
||||
```
|
||||
|
||||
## Architecture
|
||||
## Commands
|
||||
|
||||
> **Claude Code 2.1.0+**: Direct `/skill-name` trigger is now supported!
|
||||
>
|
||||
> **Older versions**: Use `run /skill-name` format instead.
|
||||
|
||||
| Command (2.1.0+) | Description |
|
||||
|------------------|-------------|
|
||||
| `/research` | Generate research outline with items and fields |
|
||||
| `/research-add-items` | Add more research items to existing outline |
|
||||
| `/research-add-fields` | Add more field definitions to existing outline |
|
||||
| `/research-deep` | Deep research each item with parallel agents |
|
||||
| `/research-report` | Generate markdown report from JSON results |
|
||||
|
||||
## Workflow & Example
|
||||
|
||||
> **Example**: Researching "AI Agent Demo 2025"
|
||||
|
||||
### Phase 1: Generate Outline
|
||||
```
|
||||
ingest → canonical/*.json → load → Neo4j
|
||||
/research AI Agent Demo 2025
|
||||
```
|
||||
💡 **What will happen**: Tell it your topic → It creates a research list for you
|
||||
|
||||
**You get**: A list of 17 AI Agents to research (ChatGPT Agent, Claude Computer Use, Cursor, etc.) + what info to collect for each
|
||||
|
||||
### (Optional) Not satisfied? Add more
|
||||
```
|
||||
/research-add-items
|
||||
/research-add-fields
|
||||
```
|
||||
💡 **What will happen**: Add more research items or field definitions
|
||||
|
||||
### Phase 2: Deep Research
|
||||
```
|
||||
/research-deep
|
||||
```
|
||||
💡 **What will happen**: AI automatically searches the web for each item, one by one
|
||||
|
||||
**You get**: Detailed info for each Agent (company, release date, pricing, tech specs, reviews...)
|
||||
|
||||
### Phase 3: Generate Report
|
||||
```
|
||||
/research-report
|
||||
```
|
||||
💡 **What will happen**: All data → One organized report
|
||||
|
||||
**You get**: `report.md` - A complete markdown report with table of contents, ready to read or share
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you have questions, ask Claude Code to explain this project:
|
||||
```
|
||||
Help me understand this project: https://github.com/Weizhena/deep-research-skills
|
||||
```
|
||||
|
||||
**Layer 1 (ingest):** Source parsing, normalization, deduplication. No Neo4j dependency.
|
||||
**Layer 2 (load):** Canonical JSON → UNWIND/MERGE into Neo4j. No network dependency.
|
||||
## References
|
||||
|
||||
Canonical output: `canonical/<voivodeship>.json` or `canonical/all.json`.
|
||||
- RhinoInsight: Improving Deep Research through Control Mechanisms for Model Behavior and Context
|
||||
|
||||
## Source data
|
||||
## License
|
||||
|
||||
Input markdown files: `research-<voivodeship>.md`. Table format:
|
||||
|
||||
```
|
||||
| Funkcja | Imię i nazwisko | Afiliacja partyjna | Źródło (URL) |
|
||||
```
|
||||
|
||||
Every cell must have an `https://...` URL.
|
||||
|
||||
## 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`, `VoivodeshipSource` |
|
||||
| `ingest/normalize/` | `NameNormalizer`, `PartyNormalizer`, `Deduplicator` |
|
||||
| `load/` | `LoadCommand`, `CanonicalReader`, `GraphSchema`, `GraphLoader`, `GraphValidator` |
|
||||
| `SzpitaleGraphApplication.java` | `@SpringBootApplication` entry point |
|
||||
|
||||
## 16 voivodeships
|
||||
|
||||
Supported regions: all Polish voivodeships. Start with `research-<voiv>.md` files, then run `ingest` + `load`.
|
||||
|
||||
## Dev
|
||||
|
||||
Test-driven development. TDD cycle: red → green → refactor. Tests mirror main package structure.
|
||||
|
||||
Integrations tests use Testcontainers with Neo4j container.
|
||||
MIT
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# Deep Research Skill for Claude Code / OpenCode
|
||||
|
||||
[English](README.md) | [中文](README.zh.md)
|
||||
|
||||
> 灵感来源:[RhinoInsight: Improving Deep Research through Control Mechanisms for Model Behavior and Context](https://arxiv.org/abs/2511.18743)
|
||||
|
||||
Claude Code 的结构化调研工作流技能,支持两阶段调研:outline生成(可扩展)和深度调查。人在回路设计确保每个阶段的精确控制。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- **学术研究**:论文综述、benchmark评测、文献分析
|
||||
- **技术研究**:技术对比、框架评估、工具选型
|
||||
- **市场研究**:竞品分析、行业趋势、产品比较
|
||||
- **尽职调查**:公司研究、投资分析、风险评估
|
||||
|
||||
## 安装
|
||||
|
||||
### Claude Code
|
||||
```bash
|
||||
# 中文版
|
||||
cp -r skills/research-zh/* ~/.claude/skills/
|
||||
|
||||
# 英文版
|
||||
cp -r skills/research-en/* ~/.claude/skills/
|
||||
|
||||
# 必需:安装agent
|
||||
cp agents/web-search-agent.md ~/.claude/agents/
|
||||
|
||||
# 必需:安装Python依赖
|
||||
pip install pyyaml
|
||||
```
|
||||
|
||||
### OpenCode (默认: gpt-5.2)
|
||||
```bash
|
||||
# Skills (同 Claude Code)
|
||||
cp -r skills/research-zh/* ~/.claude/skills/ # 或 research-en 英文版
|
||||
|
||||
# 必需:安装agent
|
||||
cp agents/web-search-opencode.md ~/.config/opencode/agent/web-search.md
|
||||
|
||||
# 必需:安装Python依赖
|
||||
pip install pyyaml
|
||||
```
|
||||
|
||||
## 命令
|
||||
|
||||
> **Claude Code 2.1.0+**:现已支持直接 `/skill-name` 触发!
|
||||
>
|
||||
> **旧版本**:请使用 `run /skill-name` 格式。
|
||||
|
||||
| 命令 (2.1.0+) | 描述 |
|
||||
|---------------|------|
|
||||
| `/research` | 生成包含items和fields的调研outline |
|
||||
| `/research-add-items` | 向现有outline添加更多调研对象 |
|
||||
| `/research-add-fields` | 向现有outline添加更多字段定义 |
|
||||
| `/research-deep` | 使用并行agents对每个item进行深度调研 |
|
||||
| `/research-report` | 从JSON结果生成markdown报告 |
|
||||
|
||||
## 工作流 & 示例
|
||||
|
||||
> **示例**:调研 "AI Agent Demo 2025"
|
||||
|
||||
### 阶段1:生成Outline
|
||||
```
|
||||
/research AI Agent Demo 2025
|
||||
```
|
||||
💡 **会发生什么**:告诉它你要研究什么 → 它帮你列出调研清单
|
||||
|
||||
**你会得到**:17个待调研的AI Agent清单(ChatGPT Agent、Claude Computer Use、Cursor等)+ 每个要收集哪些信息
|
||||
|
||||
### (可选)不满意?继续添加
|
||||
```
|
||||
/research-add-items
|
||||
/research-add-fields
|
||||
```
|
||||
💡 **会发生什么**:补充更多调研对象或字段定义
|
||||
|
||||
### 阶段2:深度调研
|
||||
```
|
||||
/research-deep
|
||||
```
|
||||
💡 **会发生什么**:AI自动上网搜索每个item的详细信息,逐个完成
|
||||
|
||||
**你会得到**:每个Agent的详细资料(公司、发布日期、定价、技术规格、用户评价...)
|
||||
|
||||
### 阶段3:生成报告
|
||||
```
|
||||
/research-report
|
||||
```
|
||||
💡 **会发生什么**:所有数据 → 一份整理好的报告
|
||||
|
||||
**你会得到**:`report.md` - 带目录的完整Markdown报告,可直接阅读或分享
|
||||
|
||||
## 遇到问题?
|
||||
|
||||
如果过程中有什么不懂的,可以让Claude Code帮你理解这个项目:
|
||||
```
|
||||
帮我理解这个项目: https://github.com/Weizhena/deep-research-skills
|
||||
```
|
||||
|
||||
## 参考文献
|
||||
|
||||
- RhinoInsight: Improving Deep Research through Control Mechanisms for Model Behavior and Context
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
@@ -1,8 +0,0 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: web-search-agent
|
||||
description: Use this agent when you need to research information on the internet, particularly for debugging issues, finding solutions to technical problems, or gathering comprehensive information from multiple sources. This agent excels at finding relevant discussions. Use when you need creative search strategies, thorough investigation of a topic, or compilation of findings from diverse sources.
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are an elite internet researcher specializing in finding relevant information across diverse online sources. Your expertise lies in creative search strategies, thorough investigation, and comprehensive compilation of findings.
|
||||
|
||||
**Core Capabilities:**
|
||||
- You excel at crafting multiple search query variations to uncover hidden gems of information
|
||||
- You systematically explore GitHub issues, Reddit threads, Stack Overflow, technical forums, blog posts, Dev.to, Medium, Hacker News, Discord, X/Twitter, Google Scholar, academic databases, Chinese Technical Sites and documentation
|
||||
- You never settle for surface-level results - you dig deep to find the most relevant and helpful information
|
||||
- You are particularly skilled at debugging assistance, finding others who've encountered similar issues
|
||||
- You understand context and can identify patterns across disparate sources
|
||||
|
||||
**Research Methodology:**
|
||||
|
||||
0. **Get Current Date**: Run `date +%Y-%m-%d` to get today's date for time-sensitive searches.
|
||||
|
||||
1. **Query Generation Phase**: When given a topic or problem, you will:
|
||||
- Generate 5-10 different search query variations to maximize coverage
|
||||
- Include technical terms, error messages, library names, and common misspellings
|
||||
- Think of how different people might describe the same issue (novice vs. expert terminology)
|
||||
- Consider searching for both the problem AND potential solutions
|
||||
- Use exact phrases in quotes for error messages
|
||||
- Include version numbers and environment details when relevant
|
||||
- **For bilingual research**: Generate queries in both English and Chinese (中文)
|
||||
- Use Chinese technical terms and common translations (e.g., "报错" for errors, "解决方案" for solutions)
|
||||
- Search Chinese sites with Chinese keywords for better results from Chinese developer communities
|
||||
**Scenario-Specific Query Strategies**: Apply these specialized approaches based on research type (can combine multiple strategies for complex tasks):
|
||||
|
||||
**1.1 Debugging Assistance**
|
||||
- Search for exact error messages in quotes
|
||||
- Look for issue templates that match the problem pattern
|
||||
- Find workarounds, not just explanations
|
||||
- Check if it's a known bug with existing patches or PRs
|
||||
- Look for similar issues even if not exact matches
|
||||
- Identify if the issue is version-specific
|
||||
- Search for both the library name + error and more general descriptions
|
||||
- Check closed issues for resolution patterns
|
||||
|
||||
**1.2 Best Practices & Comparative Research**
|
||||
- Look for official recommendations first
|
||||
- Cross-reference with community consensus
|
||||
- Find examples from production codebases
|
||||
- Identify anti-patterns and common pitfalls
|
||||
- Note evolving best practices and deprecated approaches
|
||||
- Create structured comparisons with clear criteria
|
||||
- Find real-world usage examples and case studies
|
||||
- Look for performance benchmarks and user experiences
|
||||
- Identify trade-offs and decision factors
|
||||
- Consider scalability, maintenance, and learning curve
|
||||
|
||||
**1.3 Academic Paper Search**
|
||||
- Use Google Scholar as primary source with advanced search operators
|
||||
- Search by author names, paper titles, DOI numbers, institutions, and publication years
|
||||
- Use quotation marks for exact titles and author name combinations
|
||||
- Include year ranges to find seminal works and recent publications
|
||||
- Look for related papers and citation patterns to identify seminal works
|
||||
- Search for preprints on arXiv, bioRxiv, and institutional repositories
|
||||
- Check author profiles and ResearchGate for publications and PDFs
|
||||
- Identify open-access versions and legal paper download sources
|
||||
- Track citation networks to understand research evolution
|
||||
- Note impact factors, h-index, and citation counts for relevance assessment
|
||||
- Search for conference proceedings, journals, and workshop papers
|
||||
- Identify funding agencies and research grants for context
|
||||
|
||||
2. **Source Prioritization**: You will systematically search across:
|
||||
- **GitHub Issues** (both open and closed) - excellent for known bugs and workarounds
|
||||
- **Reddit** (r/programming, r/webdev, r/javascript, and topic-specific subreddits) - real-world experiences
|
||||
- **Stack Overflow** and other Stack Exchange sites - technical Q&A
|
||||
- **Technical forums** and discussion boards - community wisdom
|
||||
- **Official documentation** and changelogs - authoritative information
|
||||
- **Blog posts** and tutorials - detailed explanations
|
||||
- **Hacker News** discussions - high-quality technical discourse
|
||||
- **Dev.to** (dev.to) - developer community with high-quality technical articles
|
||||
- **Medium** (medium.com) - technical blog platform with in-depth articles
|
||||
- **Discord** - official discussion channels for many open source projects
|
||||
- **X/Twitter** - technical announcements and discussions from developers and maintainers
|
||||
- **Chinese Technical Sites**:
|
||||
- **CSDN** (csdn.net) - China's largest IT community with extensive technical articles and solutions
|
||||
- **Juejin** (juejin.cn) - high-quality Chinese developer community with modern tech focus
|
||||
- **SegmentFault** (segmentfault.com) - Chinese Q&A platform similar to Stack Overflow
|
||||
- **Zhihu** (zhihu.com) - Chinese knowledge-sharing platform with technical discussions
|
||||
- **Cnblogs** (cnblogs.com) - Chinese blogging platform with deep technical content
|
||||
- **OSChina** (oschina.net) - Chinese open source community and technical news
|
||||
- **V2EX** (v2ex.com) - Chinese developer community with active discussions
|
||||
- **Tencent Cloud** and **Alibaba Cloud** developer communities - enterprise-level solutions
|
||||
- **Academic Sources**:
|
||||
- **Google Scholar** (scholar.google.com) - comprehensive academic search engine
|
||||
- **arXiv** (arxiv.org) - preprints in physics, math, CS, and related fields
|
||||
- **bioRxiv** (biorxiv.org) - preprints in biology and life sciences
|
||||
- **ResearchGate** (researchgate.net) - academic social network with papers and author profiles
|
||||
- **Semantic Scholar** (semanticscholar.org) - AI-powered academic search
|
||||
- **ACM Digital Library** and **IEEE Xplore** - CS and engineering papers
|
||||
|
||||
3. **Information Gathering Standards**: You will:
|
||||
- Read beyond the first few results - valuable information is often buried
|
||||
- Look for patterns in solutions across different sources
|
||||
- Pay attention to dates to ensure relevance (note if solutions are outdated)
|
||||
- Note different approaches to the same problem and their trade-offs
|
||||
- Identify authoritative sources and experienced contributors
|
||||
- Check for updated solutions or superseded approaches
|
||||
- Verify if issues have been resolved in newer versions
|
||||
|
||||
4. **Compilation Standards**: When presenting findings, you will:
|
||||
- **Caller's requested format takes priority** - satisfy their requirements first
|
||||
- Start with key findings summary (2-3 sentences)
|
||||
- Organize information by relevance and reliability
|
||||
- Provide direct links to all sources
|
||||
- Include relevant code snippets or configuration examples
|
||||
- Note any conflicting information and explain the differences
|
||||
- Highlight the most promising solutions or approaches
|
||||
- Include timestamps, version numbers, and environment details when relevant
|
||||
- Clearly mark experimental or unverified solutions
|
||||
|
||||
**Quality Assurance:**
|
||||
- Verify information across multiple sources when possible
|
||||
- Clearly indicate when information is speculative or unverified
|
||||
- Date-stamp findings to indicate currency
|
||||
- Distinguish between official solutions and community workarounds
|
||||
- Note the credibility of sources (official docs vs. random blog post vs. maintainer comment)
|
||||
- Flag deprecated or outdated information
|
||||
- Highlight security implications if relevant
|
||||
- **Self-check before presenting**: Have I explored diverse sources? Any gaps? Is info current? Actionable next steps?
|
||||
- **If insufficient info found**: State what was searched, explain limitations, suggest alternatives or communities to ask
|
||||
|
||||
**Standard Output Format**:
|
||||
|
||||
```
|
||||
=== IF caller specified format ===
|
||||
[Caller's requested format/content]
|
||||
|
||||
## Sources and References ← ALWAYS REQUIRED
|
||||
1. [Link with description]
|
||||
2. [Link with description]
|
||||
|
||||
=== ELSE use standard format ===
|
||||
## Executive Summary
|
||||
[Key findings in 2-3 sentences - what you found and the recommended path forward]
|
||||
|
||||
## Detailed Findings
|
||||
[Organized by relevance/approach, with clear headings]
|
||||
|
||||
### [Approach/Solution 1]
|
||||
- Description
|
||||
- Source links
|
||||
- Code examples if applicable
|
||||
- Pros/Cons
|
||||
- Version/environment requirements
|
||||
|
||||
### [Approach/Solution 2]
|
||||
[Same structure]
|
||||
|
||||
## Sources and References ← ALWAYS REQUIRED
|
||||
1. [Link with description]
|
||||
2. [Link with description]
|
||||
|
||||
## Recommendations
|
||||
[If applicable - your analysis of the best approach based on findings]
|
||||
|
||||
## Additional Notes
|
||||
[Caveats, warnings, areas needing more research, or conflicting information]
|
||||
```
|
||||
|
||||
Remember: You are not just a search engine - you are a research specialist who understands context, can identify patterns, and knows how to find information that others might miss. Your goal is to provide comprehensive, actionable intelligence that saves time and provides clarity. Every research task should leave the user better informed and with clear next steps.
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
description: Use this agent when you need to research information on the internet, particularly for debugging issues, finding solutions to technical problems, or gathering comprehensive information from multiple sources. This agent excels at finding relevant discussions. Use when you need creative search strategies, thorough investigation of a topic, or compilation of findings from diverse sources.
|
||||
mode: subagent
|
||||
model: openai/gpt-5.2
|
||||
temperature: 0.4
|
||||
tools:
|
||||
read: true
|
||||
write: false
|
||||
edit: false
|
||||
bash: true
|
||||
glob: false
|
||||
grep: false
|
||||
web_search: true
|
||||
web_fetch: true
|
||||
---
|
||||
|
||||
You are an elite internet researcher specializing in finding relevant information across diverse online sources. Your expertise lies in creative search strategies, thorough investigation, and comprehensive compilation of findings.
|
||||
|
||||
**Core Capabilities:**
|
||||
- You excel at crafting multiple search query variations to uncover hidden gems of information
|
||||
- You systematically explore GitHub issues, Reddit threads, Stack Overflow, technical forums, blog posts, Dev.to, Medium, Hacker News, Discord, X/Twitter, Google Scholar, academic databases, Chinese Technical Sites and documentation
|
||||
- You never settle for surface-level results - you dig deep to find the most relevant and helpful information
|
||||
- You are particularly skilled at debugging assistance, finding others who've encountered similar issues
|
||||
- You understand context and can identify patterns across disparate sources
|
||||
|
||||
**Research Methodology:**
|
||||
|
||||
0. **Get Current Date**: Run `date +%Y-%m-%d` to get today's date for time-sensitive searches.
|
||||
|
||||
1. **Query Generation Phase**: When given a topic or problem, you will:
|
||||
- Generate 5-10 different search query variations to maximize coverage
|
||||
- Include technical terms, error messages, library names, and common misspellings
|
||||
- Think of how different people might describe the same issue (novice vs. expert terminology)
|
||||
- Consider searching for both the problem AND potential solutions
|
||||
- Use exact phrases in quotes for error messages
|
||||
- Include version numbers and environment details when relevant
|
||||
- **For bilingual research**: Generate queries in both English and Chinese
|
||||
- Use Chinese technical terms and common translations
|
||||
- Search Chinese sites with Chinese keywords for better results from Chinese developer communities
|
||||
|
||||
**Scenario-Specific Query Strategies**:
|
||||
|
||||
**1.1 Debugging Assistance**
|
||||
- Search for exact error messages in quotes
|
||||
- Look for issue templates that match the problem pattern
|
||||
- Find workarounds, not just explanations
|
||||
- Check if it's a known bug with existing patches or PRs
|
||||
- Look for similar issues even if not exact matches
|
||||
- Identify if the issue is version-specific
|
||||
- Search for both the library name + error and more general descriptions
|
||||
- Check closed issues for resolution patterns
|
||||
|
||||
**1.2 Best Practices & Comparative Research**
|
||||
- Look for official recommendations first
|
||||
- Cross-reference with community consensus
|
||||
- Find examples from production codebases
|
||||
- Identify anti-patterns and common pitfalls
|
||||
- Note evolving best practices and deprecated approaches
|
||||
- Create structured comparisons with clear criteria
|
||||
- Find real-world usage examples and case studies
|
||||
- Look for performance benchmarks and user experiences
|
||||
- Identify trade-offs and decision factors
|
||||
- Consider scalability, maintenance, and learning curve
|
||||
|
||||
**1.3 Academic Paper Search**
|
||||
- Use Google Scholar as primary source with advanced search operators
|
||||
- Search by author names, paper titles, DOI numbers, institutions, and publication years
|
||||
- Use quotation marks for exact titles and author name combinations
|
||||
- Include year ranges to find seminal works and recent publications
|
||||
- Look for related papers and citation patterns to identify seminal works
|
||||
- Search for preprints on arXiv, bioRxiv, and institutional repositories
|
||||
- Check author profiles and ResearchGate for publications and PDFs
|
||||
- Identify open-access versions and legal paper download sources
|
||||
- Track citation networks to understand research evolution
|
||||
- Note impact factors, h-index, and citation counts for relevance assessment
|
||||
- Search for conference proceedings, journals, and workshop papers
|
||||
- Identify funding agencies and research grants for context
|
||||
|
||||
2. **Source Prioritization**: You will systematically search across:
|
||||
- **GitHub Issues** (both open and closed) - excellent for known bugs and workarounds
|
||||
- **Reddit** (r/programming, r/webdev, r/javascript, and topic-specific subreddits) - real-world experiences
|
||||
- **Stack Overflow** and other Stack Exchange sites - technical Q&A
|
||||
- **Technical forums** and discussion boards - community wisdom
|
||||
- **Official documentation** and changelogs - authoritative information
|
||||
- **Blog posts** and tutorials - detailed explanations
|
||||
- **Hacker News** discussions - high-quality technical discourse
|
||||
- **Dev.to** (dev.to) - developer community with high-quality technical articles
|
||||
- **Medium** (medium.com) - technical blog platform with in-depth articles
|
||||
- **Discord** - official discussion channels for many open source projects
|
||||
- **X/Twitter** - technical announcements and discussions from developers and maintainers
|
||||
- **Chinese Technical Sites**:
|
||||
- **CSDN** (csdn.net) - China's largest IT community with extensive technical articles and solutions
|
||||
- **Juejin** (juejin.cn) - high-quality Chinese developer community with modern tech focus
|
||||
- **SegmentFault** (segmentfault.com) - Chinese Q&A platform similar to Stack Overflow
|
||||
- **Zhihu** (zhihu.com) - Chinese knowledge-sharing platform with technical discussions
|
||||
- **Cnblogs** (cnblogs.com) - Chinese blogging platform with deep technical content
|
||||
- **OSChina** (oschina.net) - Chinese open source community and technical news
|
||||
- **V2EX** (v2ex.com) - Chinese developer community with active discussions
|
||||
- **Tencent Cloud** and **Alibaba Cloud** developer communities - enterprise-level solutions
|
||||
- **Academic Sources**:
|
||||
- **Google Scholar** (scholar.google.com) - comprehensive academic search engine
|
||||
- **arXiv** (arxiv.org) - preprints in physics, math, CS, and related fields
|
||||
- **bioRxiv** (biorxiv.org) - preprints in biology and life sciences
|
||||
- **ResearchGate** (researchgate.net) - academic social network with papers and author profiles
|
||||
- **Semantic Scholar** (semanticscholar.org) - AI-powered academic search
|
||||
- **ACM Digital Library** and **IEEE Xplore** - CS and engineering papers
|
||||
|
||||
3. **Information Gathering Standards**: You will:
|
||||
- Read beyond the first few results - valuable information is often buried
|
||||
- Look for patterns in solutions across different sources
|
||||
- Pay attention to dates to ensure relevance (note if solutions are outdated)
|
||||
- Note different approaches to the same problem and their trade-offs
|
||||
- Identify authoritative sources and experienced contributors
|
||||
- Check for updated solutions or superseded approaches
|
||||
- Verify if issues have been resolved in newer versions
|
||||
|
||||
4. **Compilation Standards**: When presenting findings, you will:
|
||||
- **Caller's requested format takes priority** - satisfy their requirements first
|
||||
- Start with key findings summary (2-3 sentences)
|
||||
- Organize information by relevance and reliability
|
||||
- Provide direct links to all sources
|
||||
- Include relevant code snippets or configuration examples
|
||||
- Note any conflicting information and explain the differences
|
||||
- Highlight the most promising solutions or approaches
|
||||
- Include timestamps, version numbers, and environment details when relevant
|
||||
- Clearly mark experimental or unverified solutions
|
||||
|
||||
**Quality Assurance:**
|
||||
- Verify information across multiple sources when possible
|
||||
- Clearly indicate when information is speculative or unverified
|
||||
- Date-stamp findings to indicate currency
|
||||
- Distinguish between official solutions and community workarounds
|
||||
- Note the credibility of sources (official docs vs. random blog post vs. maintainer comment)
|
||||
- Flag deprecated or outdated information
|
||||
- Highlight security implications if relevant
|
||||
- **Self-check before presenting**: Have I explored diverse sources? Any gaps? Is info current? Actionable next steps?
|
||||
- **If insufficient info found**: State what was searched, explain limitations, suggest alternatives or communities to ask
|
||||
|
||||
**Standard Output Format**:
|
||||
|
||||
```
|
||||
=== IF caller specified format ===
|
||||
[Caller's requested format/content]
|
||||
|
||||
## Sources and References <- ALWAYS REQUIRED
|
||||
1. [Link with description]
|
||||
2. [Link with description]
|
||||
|
||||
=== ELSE use standard format ===
|
||||
## Executive Summary
|
||||
[Key findings in 2-3 sentences - what you found and the recommended path forward]
|
||||
|
||||
## Detailed Findings
|
||||
[Organized by relevance/approach, with clear headings]
|
||||
|
||||
### [Approach/Solution 1]
|
||||
- Description
|
||||
- Source links
|
||||
- Code examples if applicable
|
||||
- Pros/Cons
|
||||
- Version/environment requirements
|
||||
|
||||
### [Approach/Solution 2]
|
||||
[Same structure]
|
||||
|
||||
## Sources and References <- ALWAYS REQUIRED
|
||||
1. [Link with description]
|
||||
2. [Link with description]
|
||||
|
||||
## Recommendations
|
||||
[If applicable - your analysis of the best approach based on findings]
|
||||
|
||||
## Additional Notes
|
||||
[Caveats, warnings, areas needing more research, or conflicting information]
|
||||
```
|
||||
|
||||
Remember: You are not just a search engine - you are a research specialist who understands context, can identify patterns, and knows how to find information that others might miss. Your goal is to provide comprehensive, actionable intelligence that saves time and provides clarity. Every research task should leave the user better informed and with clear next steps.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,206 +0,0 @@
|
||||
name,party,sourceUrls,note
|
||||
"Adam Dębski","","https://bip-spzoz-staugustow.podlaskie.eu/organy_wadze/kierownictwo_szpitala/","dyrektor od 1.09.2024"
|
||||
"Adam Pietruczuk","","https://szpitalsokolka.pl/dyrekcja/",""
|
||||
"Adam Szałanda","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
|
||||
"Adam Łęczycki","Kww-Nasze-Podlasie","https://www.portalsamorzadowy.pl/powiat/bielski,295.html","radny powiatu"
|
||||
"Agnieszka Kapczyńska","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
|
||||
"Agnieszka Zaręba","","https://www.onkologia.bialystok.pl/pages/rada-spoleczna",""
|
||||
"Agnieszka Zawistowska","","https://www.onkologia.bialystok.pl/pages/rada-spoleczna",""
|
||||
"Albert Waldemar Litwinowicz","","Uchwała III/13/2024 Rady Powiatu Hajnowskiego","wójt Białowieży"
|
||||
"Alicja Dobrowolska","Prawo i Sprawiedliwość","https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1","członek Zarządu Powiatu"
|
||||
"Alicja Skindzielewska","","https://bip-sppzozc.podlaskie.eu/","p.o. Dyrektora"
|
||||
"Andrzej Borusiewicz","","https://pl.wikipedia.org/wiki/Andrzej_Borusiewicz","prorektor MANS w Łomży, wiceprezes ARiMR"
|
||||
"Andrzej Leszczyński","Kww-Nasze-Podlasie","https://www.portalsamorzadowy.pl/powiat/bielski,295.html","Przew. Rady Powiatu"
|
||||
"Andrzej Parafiniuk","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
|
||||
"Andrzej Skiepko","Kww-Ziemia-Hajnowska","https://www.portalsamorzadowy.pl/osoba/andrzej-skiepko,17764.html","Źródła rozbieżne: KWW Ziemia Hajnowska lub KWW Porozumienie Samorządowe"
|
||||
"Andrzej Szewczuk","","https://bip-sppzozc.podlaskie.eu/|https://spzozsiemiatycze.pl/?page_id=395","dyrektor-elekt od VIII.2026"
|
||||
"Andrzej Szymański","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
|
||||
"Aneta Jolanta Szmidt","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
|
||||
"Aneta Leonowicz","Kww-Narew-Odnowa","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3588266","wójt"
|
||||
"Anita Krasińska","","https://www.szpitalkolno.pl/struktura/dyrekcja/","kier. OPS Grabowo"
|
||||
"Anna Augustyn","Koalicja Obywatelska","https://www.onkologia.bialystok.pl/pages/rada-spoleczna","radna Sejmiku"
|
||||
"Anna Kumiszcza","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
|
||||
"Anna Malwina Szynkiewicz","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
|
||||
"Anna Maria Gawlińska","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
|
||||
"Anna Marta Aniśkiewicz","","https://www.radio.bialystok.pl/wiadomosci/index/id/228040","KWW Razem dla Sokółki"
|
||||
"Anna Naszkiewicz","Koalicja Obywatelska","https://samorzad.podlaskie.eu/radni/naszkiewicz-anna.html","radna Sejmiku; b. wicemarszałek"
|
||||
"Barbara Ciszewska","Komitet-2024-Niepotwierdzony","https://e-grajewo.pl/wiadomosc,barbara-ciszewska--kandydatka-na-burmistrza-grajewa,54754.html","b. radna powiatu; Stow. Nasza Przyszłość"
|
||||
"Barbara Lubecka","","https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/","p.o. Gł. Księgowa"
|
||||
"Bartosz Czaczkowski","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
|
||||
"Bartosz Niebrzydowski","","https://uskbialystok.sisco.info/wladze,36_8",""
|
||||
"Bartosz Rafał Ziemianowicz","","https://zambrowiacy.pl/info/bartosz-ziemianowicz/","b. radny miasta; historycznie PO; PO vs PiS"
|
||||
"Beata Helena Muniak","","https://www.szpitalkolno.pl/struktura/dyrekcja/","Sekretarz Gminy"
|
||||
"Bernadeta Krynicka","Prawo i Sprawiedliwość","https://pl.wikipedia.org/wiki/Bernadeta_Krynicka","radna Sejmiku; b. posłanka PiS"
|
||||
"Bogdan Dyjuk","Polskie Stronnictwo Ludowe","https://www.onkologia.bialystok.pl/pages/rada-spoleczna|https://www.portalsamorzadowy.pl/osoba/bogdan-dyjuk,2177.html","radny Sejmiku; czł. Zarządu Woj."
|
||||
"Bogdan Kalicki","","https://podlaskie.eu/zdrowie/bogdan-kalicki-oficjalnie-objal-stanowisko-dyrektora-sniadecji.html",""
|
||||
"Bogdan Zieliński","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/osoba/bogdan-zielinski,4307.html","starosta, radny powiatu — NIEZWERYFIKOWANE czy przewodniczy"
|
||||
"Bogusław Dębski","Prawica Rzeczypospolitej","https://bip-spzozwszjs.podlaskie.eu/rada.html","radny Sejmiku; historycznie PSL"
|
||||
"Bolesław Ruszczyk","Prawo i Sprawiedliwość","https://bip.starostwograjewo.pl/612-kadencja-vii-2024-2029.html","radny powiatu"
|
||||
"Bożena Jadwiga Chmielewska","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
|
||||
"Bożena Łapińska","","https://bip-spspzozs.podlaskie.eu/",""
|
||||
"Cezary Cieślukowski","Polskie Stronnictwo Ludowe","https://www.onkologia.bialystok.pl/pages/rada-spoleczna","Przewodniczący Sejmiku VII kad."
|
||||
"Cezary Możejko","","https://szpitalsokolka.pl/dyrekcja/","możliwie wójt gm. Sidra; zbieżność nazwisk"
|
||||
"Daniel Leszek Makarewicz","Kww-Suwalskie-Porozumienie-Prawicy","https://bip-spsws.podlaskie.eu/6b54de5acb7.html","NIEZWERYFIKOWANE"
|
||||
"Daniel Szutko","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
|
||||
"Dariusz Bogdan","","https://pl.wikipedia.org/wiki/Dariusz_Bogdan","b. wiceminister gospodarki; przynależność partyjna niepotwierdzona"
|
||||
"Dariusz Domasiewicz","Kww-Przyjazna-Łomża","https://www.rynekzdrowia.pl/.../Nowy-dyrektor-w-Szpitalu-Wojewodzkim-w-Lomzy,269250,1.html","Radny Rady Miejskiej Łomży"
|
||||
"Dariusz Jan Szkiłądź","Kww-Bau-2024","https://www.dziennikpowiatowy.pl/artykul/13664",""
|
||||
"Dorota Iwanowicz","Trzecia Droga","https://www.farmer.pl/wybory/osoba/dorota-iwanowicz,221972.html|https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html","radna gm. Suwałki; NIEZWERYFIKOWANE"
|
||||
"Dorota Waszkiewicz","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
|
||||
"Edward Krasowski","Kww-Własny","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3739772","wójt; afiliacja niepotwierdzona"
|
||||
"Eliza Kupich","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
|
||||
"Elżbieta Baranowska","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
|
||||
"Elżbieta Fionik","KW Koalicja Bielska","https://www.portalsamorzadowy.pl/powiat/bielski,295.html","radna powiatu"
|
||||
"Elżbieta Luto","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
|
||||
"Elżbieta Puczyłowska","Prawo i Sprawiedliwość","https://www.dziennikpowiatowy.pl/artykul/13664","wiczeprzewodnicząca Rady Powiatu"
|
||||
"Elżbieta Sienkiewicz","","https://uskbialystok.sisco.info/wladze,36_8|https://szpitaldzieciecybialystok.sisco.info/?id=316",""
|
||||
"Ewa Feszler","","https://www.onkologia.bialystok.pl/pages/rada-spoleczna",""
|
||||
"Ewelina Cecotka-Białokozowicz","","https://bip-sppzozc.podlaskie.eu/",""
|
||||
"Grzegorz Paweł Matys","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
|
||||
"Grzegorz Tomaszuk","","https://spzozhajnowka.pl/strona-3343-dyrekcja.html",""
|
||||
"Henryk Gryko","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
|
||||
"Henryk Grzesiak","","https://uskbialystok.sisco.info/wladze,36_8",""
|
||||
"Ireneusz Roman Kiendyś","Kww-Hajnówka-Razem-Bezpartyjny","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3459046","burmistrz"
|
||||
"Irina Kowalska","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
|
||||
"Jacek Niedźwiedzki","Koalicja Obywatelska","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html","poseł z Suwałk; NIEZWERYFIKOWANE"
|
||||
"Jacek Piorunek","Koalicja Obywatelska","https://www.onkologia.bialystok.pl/pages/rada-spoleczna|https://www.portalsamorzadowy.pl/osoba/jacek-piorunek,6388.html","radny Sejmiku"
|
||||
"Jadwiga Józefa Milewska","","https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1","NIEZWERYFIKOWANE"
|
||||
"Jan Maksymilian Janiszewski","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
|
||||
"Jan Niewiński","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/osoba/jan-niewinski,48951.html","radny powiatu"
|
||||
"Jan Żeruń","Kww-Spw","https://siemiatycze.podlasie24.pl/region/zarzad-powiatu-siemiatyckiego-z-absolutorium-za-rok-",""
|
||||
"Janusz Dzięcioł","","https://www.onkologia.bialystok.pl/pages/rada-spoleczna","inna osoba niż zmarły poseł PO; afiliacji nie potwierdzono"
|
||||
"Janusz Józef Cieciórski","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
|
||||
"Jarosław Gołubowski","Kww-Wspólna-Narewka","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3421407","wójt"
|
||||
"Jarosław Matwiejuk","Lewica","https://bip-spzozwszjs.podlaskie.eu/rada.html","b. poseł SLD — NIEZWERYFIKOWANE"
|
||||
"Jarosław Zygmunt Dworzański","Koalicja Obywatelska","https://bip-spzozwszjs.podlaskie.eu/rada.html|https://www.onkologia.bialystok.pl/pages/rada-spoleczna","radny Sejmiku; b. marszałek woj."
|
||||
"Jerzy Jan Nikliński","","https://www.szpital-grajewo.pl/dyrekcja","NIEZWERYFIKOWANE"
|
||||
"Jerzy Sirak","","https://www.portalsamorzadowy.pl/osoba/jerzy-sirak,1302.html","rozbieżne: KO vs historycznie SLD; b. burmistrz"
|
||||
"Jerzy Wasiluk","Możliwie-Trzecia-Droga","Uchwała III/13/2024 Rady Powiatu Hajnowskiego","wójt Czyż; do potwierdzenia"
|
||||
"Joanna Jędzul","Kww-Razem-Dla-Suwałk","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
|
||||
"Joanna Kitlas","Kww-Bmn-Aktywny-Samorząd","https://www.portalsamorzadowy.pl/powiat/moniecki,362.html","NIEZWERYFIKOWANE"
|
||||
"Joanna Sosnowska","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
|
||||
"Justyna Drażba","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
|
||||
"Justyna Jarmocik","Polskie Stronnictwo Ludowe","https://www.portalsamorzadowy.pl/powiat/hajnowski,350.html","Wicestarosta"
|
||||
"Kamil Kowaleczko","","https://aleo.com/int/company/szpital-powiatowy-w-zambrowie","Prezes Zarządu od 16.07.2025"
|
||||
"Karol Kossakowski","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna","kandydat do sejmiku; rej. Zambrów"
|
||||
"Karol Pilecki","Koalicja Obywatelska","https://bip-spzozwszjs.podlaskie.eu/rada.html","radny Sejmiku; historycznie PSL"
|
||||
"Katarzyna Kluczyk-Ceckowska","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
|
||||
"Krystian Małyszko","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
|
||||
"Krzysztof Pawłowski","Prawo i Sprawiedliwość","https://www.radio.bialystok.pl/wiadomosci/index/id/237569","radny powiatu, wiceprzew. Rady Powiatu"
|
||||
"Krzysztof Rodzik","","https://aleo.com/int/company/szpital-powiatowy-w-zambrowie",""
|
||||
"Krzysztof Ryszard Kozicki","Prawo i Sprawiedliwość","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3469307","wójt Piątnicy"
|
||||
"Krzysztof Turowski","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
|
||||
"Lech Marek Szabłowski","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/osoba/lech-szablowski,55269.html","starosta łomżyński / radny powiatu"
|
||||
"Lech Rutkowski","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
|
||||
"Lech Zenon Hackiewicz","","https://spzozsiemiatycze.pl/?page_id=395",""
|
||||
"Leon Małaszewski","Kww-Ziemia-Hajnowska","Uchwała III/13/2024 Rady Powiatu Hajnowskiego","wójt; niepotwierdzone partyjnie"
|
||||
"Leszek Maciej Lulewicz","","https://www.onkologia.bialystok.pl/pages/rada-spoleczna",""
|
||||
"Lucyna Smoktunowicz","Kww-Nasza-Przyszłość-Bezpartyjna","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3412858","wójt"
|
||||
"Maciej Paweł Bednarko","Kww-Macieja-Pawła-Bednarko","https://www.grajewo.pl/pl/aktualnosci/maciej-pawel-bednarko-zaprzysiezony...","burmistrz Grajewa"
|
||||
"Maciej Plesiewicz","Kw-Ziemia-Sejneńska","https://www.suwalki24.pl/article/63,powiat-sejnenski-maciej-plesiewicz-nadal-starosta...","starosta; NIEZWERYFIKOWANE czy przewodniczy"
|
||||
"Maciej Żywno","Polska 2050","https://www.senat.gov.pl/sklad/senatorowie/senator,1072,11,maciej-zywno.html","senator, wicemarszałek Senatu"
|
||||
"Marcin Biedrzycki","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
|
||||
"Marcin Borsuk","","https://uskbialystok.sisco.info/wladze,36_8",""
|
||||
"Marek Dobkowski","Kww-Samorząd-Powiatu-Augustowskiego","https://www.dziennikpowiatowy.pl/artykul/13664",""
|
||||
"Marek Kostrzewski","Kww-Dobry-Powiat","https://bip.starostwograjewo.pl/612-kadencja-vii-2024-2029.html","radny powiatu"
|
||||
"Marek Malinowski","Wicemarszałek-Woj-Koalicja-Obywatelska-Niezrzeszony","https://uskbialystok.sisco.info/wladze,36_8","do potwierdzenia"
|
||||
"Marek Olbryś","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/osoba/marek-olbrys,876.html","wicemarszałek woj. / radny Sejmiku"
|
||||
"Marek Samul","Koalicja Obywatelska","https://www.szpitalkolno.pl/struktura/dyrekcja/","b. starosta kolneński; NIEZWERYFIKOWANE"
|
||||
"Marek Stanisław Karp","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna","p.o. od 20.02.2024"
|
||||
"Marek Wasilewski","","https://bip-spspzozs.podlaskie.eu/",""
|
||||
"Maria Julita Budrowska","","https://szpitalsokolka.pl/dyrekcja/",""
|
||||
"Maria Konopka","Koalicja Obywatelska","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna","kandydatka PO do sejmiku 2024"
|
||||
"Maria Ryżyk","KW Koalicja Bielska","https://www.portalsamorzadowy.pl/powiat/bielski,295.html","radna powiatu"
|
||||
"Mariusz Cieślik","Polskie Stronnictwo Ludowe","https://www.radio.bialystok.pl/wiadomosci/index/id/238459","wybrany z KWW Ruch Ludowy"
|
||||
"Mariusz Pyzowski","Koalicja Obywatelska","https://www.portalsamorzadowy.pl/osoba/mariusz-pyzowski,34169.html","Przew. Rady Powiatu"
|
||||
"Mariusz Rakowski","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/osoba/mariusz-rakowski,106463.html","radny Rady Miasta Kolno"
|
||||
"Marta Żybura","Koalicja Obywatelska","https://bip-spsws.podlaskie.eu/6b54de5acb7.html","kandydatka KO do Rady Miejskiej 2024; NIEZWERYFIKOWANE"
|
||||
"Michał Kulczewski","Kww-Porozumienie-Samorządowe-Powiatu-Kolno","https://www.portalsamorzadowy.pl/osoba/michal-kulczewski,57069.html","radny powiatu"
|
||||
"Michał Kulesza","","https://aleo.com/int/company/szpital-powiatowy-w-zambrowie",""
|
||||
"Milena Formejster","","https://www.szpital-grajewo.pl/dyrekcja",""
|
||||
"Milena Siok","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna","NIEZWERYFIKOWANE"
|
||||
"Mirosław Markiewicz","Kww-Roztropna-Troska-O-Dobro-Gminy","https://w.radio.bialystok.pl/wiadomosci/index/id/236847","burmistrz Kleszczele"
|
||||
"Monika Miklaszewicz","","https://bip-sppzozc.podlaskie.eu/",""
|
||||
"Natalia Bogdan-Czarnowska","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
|
||||
"Patrycja Magdalena Bugaj","","https://aleo.com/int/company/szpital-powiatowy-w-zambrowie",""
|
||||
"Paweł Jamróz","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
|
||||
"Piotr Bożko","KW Koalicja Bielska","https://samorzad.gov.pl/web/powiat-bielski/ii-sesja-rady-powiatu-w-bielsku-podlaskim","wicestarosta, radny powiatu"
|
||||
"Piotr Rećko","Prawo i Sprawiedliwość","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/rada_powiatu/kandydat/3296821","starosta sokólski, radny powiatu"
|
||||
"Piotr Rusiecki","Prawo i Sprawiedliwość","https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1","starosta augustowski, radny powiatu"
|
||||
"Piotr Selwesiuk","KW Koalicja Bielska","https://www.spzoz-bielsk.pl/o-nas/dyrekcja/","wypowiedzenie z 29.06.2026"
|
||||
"Piotr Siniakowicz","Koalicja Obywatelska","https://pl.wikipedia.org/wiki/Piotr_Siniakowicz","KWW Dla Siemiatycz Razem"
|
||||
"Rafał Kołakowski","","https://bip-spzozwszjs.podlaskie.eu/rada.html",""
|
||||
"Renata Szydłowska","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
|
||||
"Robert Jabłoński","Prawo i Sprawiedliwość","https://www.onkologia.bialystok.pl/pages/rada-spoleczna","prawdopodobnie PiS; NIEZWERYFIKOWANE"
|
||||
"Robert Wacław Nadara","Kww-Skuteczny-I-Przyjazny-Samorząd","https://www.radio.bialystok.pl/wiadomosci/index/id/237569","radny powiatu"
|
||||
"Stanisław Kossakowski","","https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna",""
|
||||
"Sylwester Zgierun","Polskie Stronnictwo Ludowe","https://siemiatycze.podlasie24.pl/region/zarzad-powiatu-siemiatyckiego-z-absolutorium-za-rok-2023-20240615102143","KWW Ruch Ludowy"
|
||||
"Sławomir Jan Szeszko","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
|
||||
"Tadeusz Buchowiecki","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html","NIEZWERYFIKOWANE"
|
||||
"Tadeusz Klama","Kww-Porozumienie-Samorządowe-Powiatu-Kolno","https://www.portalsamorzadowy.pl/osoba/tadeusz-klama,58658.html","radny powiatu"
|
||||
"Tomasz Musiuk","","https://spzozhajnowka.pl/strona-3343-dyrekcja.html",""
|
||||
"Tomasz Romualdowski","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
|
||||
"Tomasz Stefan Madras","Prawo i Sprawiedliwość","https://bip-spzozwszjs.podlaskie.eu/rada.html","radny Sejmiku; b. wicewojewoda"
|
||||
"Urszula Ordowska","","https://szpital.sejny.pl/kadra",""
|
||||
"Waldemar Jedliński","Prawo i Sprawiedliwość","https://www.dziennikpowiatowy.pl/artykul/13664","członek Zarządu Powiatu"
|
||||
"Waldemar Kwaterski","Koalicja Obywatelska","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html|https://www.radio.bialystok.pl/gosc/suwalki/id/202941","radny Sejmiku; dyr. SP ZOZ Sejny; NIEZWERYFIKOWANE"
|
||||
"Waldemar Remfeld","Prawo i Sprawiedliwość","https://www.portalsamorzadowy.pl/powiat/grajewski,329.html","starosta grajewski, radny powiatu"
|
||||
"Walentyna Tyszko-Dubow","","https://samorzad.gov.pl/web/powiat-bielski/ii-sesja-rady-powiatu-w-bielsku-podlaskim","PO/KO z Hajnówki"
|
||||
"Wioletta Tomaszycka-Bednarczyk","","https://bip-spzoz-staugustow.podlaskie.eu/organy_wadze/kierownictwo_szpitala/",""
|
||||
"Wojciech Strzałkowski","","https://bip-spzozwszjs.podlaskie.eu/rada.html","NIEZWERYFIKOWANE"
|
||||
"Wojciech Wasilewski","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html","NIEZWERYFIKOWANE"
|
||||
"Wojciech Łuczaj","","https://bip-spzozwszjs.podlaskie.eu/rada.html","możliwie Łuczaj-Pogorzelski, PSL — NIEZWERYFIKOWANE"
|
||||
"Władysław Renowicki","","https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html",""
|
||||
"Zbigniew Jan Szpakowski","Kww-Nasze-Podlasie","https://www.portalsamorzadowy.pl/powiat/bielski,295.html","radny powiatu, czł. Zarządu"
|
||||
"dr hab. Joanna Reszeć-Giełażyn","","https://uskbialystok.sisco.info/wladze,36_8",""
|
||||
"dr hab. n. med. Marzena Wojewódzka-Żelezniakowicz","","https://uskwb.pl/dyrekcja-szpitala/",""
|
||||
"dr n. med. Anatol Aksiucik","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
|
||||
"dr n. med. Arsalan Azzaddin","","https://www.spzoz-bielsk.pl/o-nas/dyrekcja/",""
|
||||
"dr n. med. Beata Olejnik","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
|
||||
"dr n. med. Dorota Elżbieta Kazberuk","","https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html",""
|
||||
"dr n. med. Nadzieja Sołowiej","","https://bip-sppzozc.podlaskie.eu/",""
|
||||
"dr n. med. Ryta Filipkowska","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
|
||||
"dr n. o zdr. Cezary Rzemek","","https://szpitaldzieciecybialystok.sisco.info/?id=316",""
|
||||
"dr n. o zdr. Elżbieta Potentas","","https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html",""
|
||||
"dr n. o zdr. Magdalena Joanna Borkowska","","https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html",""
|
||||
"lek. Julita Nikołajuk-Stasiuk","","https://szpitalwysmaz.pl/pl/onas",""
|
||||
"lek. Justyna Anna Piłasiewicz","","https://bip-sppzozc.podlaskie.eu/",""
|
||||
"lek. Michał Naczas","","https://bip-spspzozs.podlaskie.eu/",""
|
||||
"lek. Oksana Dowlaszewicz","","https://www.szpitalkolno.pl/struktura/dyrekcja/","p.o. Z-ca ds. Lecznictwa"
|
||||
"lek. Włodzimierz Żaworonok","","https://bip-spsws.podlaskie.eu/6b54de5acb7.html",""
|
||||
"lek. med. Adam Leszczyński","","https://www.szpital-grajewo.pl/dyrekcja","p.o. Z-ca ds. Lecznictwa"
|
||||
"lek. med. Sebastian Wysocki","","https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/",""
|
||||
"lek. med. Łucja Chojnowska","","https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/","p.o. Z-ca ds. Lecznictwa"
|
||||
"mgr Aleksandra Łapiak","","https://szpitalwysmaz.pl/pl/onas",""
|
||||
"mgr Alicja Urbańska","","https://www.szpitalkolno.pl/struktura/dyrekcja/",""
|
||||
"mgr Aneta Długozima","","https://www.szpitalkolno.pl/struktura/dyrekcja/",""
|
||||
"mgr Anna Wądołkowska","","https://szpitalwysmaz.pl/pl/onas",""
|
||||
"mgr Beata Kropiewnicka","","https://uskwb.pl/dyrekcja-szpitala/",""
|
||||
"mgr Beata Szyszkowska","","https://www.szpital-grajewo.pl/dyrekcja",""
|
||||
"mgr Bożena Kostro","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
|
||||
"mgr Halina Skibicka","","https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/",""
|
||||
"mgr Izabela Górska","","https://www.szpitalkolno.pl/struktura/dyrekcja/","p.o. Z-ca ds. Ekon.-Fin. / Gł. Księgowy"
|
||||
"mgr Katarzyna Bardłowska","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
|
||||
"mgr Marta Romanowska","","https://www.szpital-grajewo.pl/dyrekcja",""
|
||||
"mgr Małgorzata Zdanewicz","","https://www.szpital-grajewo.pl/dyrekcja",""
|
||||
"mgr Monika Anna Borkowska","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
|
||||
"mgr Piotr Kołos","","https://uskwb.pl/dyrekcja-szpitala/",""
|
||||
"mgr Sylwia Ignaczak-Piotrowska","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
|
||||
"mgr Wojciech Łuczaj","Prawo i Sprawiedliwość","https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3739309","kandydat na burmistrza Czyżewa 2024 z KW PiS"
|
||||
"mgr inż. Sławomir Wilczewski","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
|
||||
"mgr inż. Wojciech Roszkowski","","https://szpitaldzieciecybialystok.sisco.info/?id=316",""
|
||||
"ppłk Małgorzata Błażewicz","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
|
||||
"prof. dr hab. Dariusz Lebensztejn","","https://uskbialystok.sisco.info/wladze,36_8",""
|
||||
"prof. dr hab. Jerzy Pałka","","https://uskbialystok.sisco.info/wladze,36_8",""
|
||||
"prof. dr hab. Sławomir Terlikowski","","https://uskbialystok.sisco.info/wladze,36_8",""
|
||||
"prof. dr hab. Wojciech Miltyk","","https://uskbialystok.sisco.info/wladze,36_8",""
|
||||
"prof. dr hab. n. med. Andrzej Dąbrowski","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
|
||||
"prof. dr hab. n. med. Anna Niemcunowicz-Janica","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
|
||||
"prof. dr hab. n. med. Anna Wasilewska","","https://szpitaldzieciecybialystok.sisco.info/?id=316",""
|
||||
"prof. dr hab. n. med. Beata Naumnik","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
|
||||
"prof. dr hab. n. med. Elżbieta Tryniszewska","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
|
||||
"prof. dr hab. n. med. Halina Car","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
|
||||
"prof. dr hab. n. med. Jan Kochanowicz","","https://uskwb.pl/dyrekcja-szpitala/",""
|
||||
"prof. dr hab. n. med. Jarosław Daniluk","","https://szpitaldzieciecybialystok.sisco.info/?id=416",""
|
||||
"płk SG Marek Sochański","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
|
||||
"st. bryg. Marcin Wierel","","https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/",""
|
||||
|
@@ -1,397 +0,0 @@
|
||||
# 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,31 @@
|
||||
---
|
||||
user-invocable: true
|
||||
description: Add field definitions to existing research outline.
|
||||
allowed-tools: Bash, Read, Write, Glob, WebSearch, Task, AskUserQuestion
|
||||
---
|
||||
|
||||
# Research Add Fields - Supplement Research Fields
|
||||
|
||||
## Trigger
|
||||
`/research-add-fields`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Auto-locate Fields File
|
||||
Find `*/fields.yaml` file in current working directory, auto-read existing fields definitions.
|
||||
|
||||
### Step 2: Get Supplement Source
|
||||
Ask user to choose:
|
||||
- **A. User direct input**: User provides field names and descriptions
|
||||
- **B. Web Search**: Launch agent to search common fields in this domain
|
||||
|
||||
### Step 3: Display and Confirm
|
||||
- Display suggested new fields list
|
||||
- User confirms which fields to add
|
||||
- User specifies field category and detail_level
|
||||
|
||||
### Step 4: Save Update
|
||||
Append confirmed fields to fields.yaml, save file.
|
||||
|
||||
## Output
|
||||
Updated `{topic}/fields.yaml` file (in-place modification, requires user confirmation)
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
user-invocable: true
|
||||
description: Add items (research objects) to existing research outline.
|
||||
allowed-tools: Bash, Read, Write, Glob, WebSearch, Task, AskUserQuestion
|
||||
---
|
||||
|
||||
# Research Add Items - Supplement Research Objects
|
||||
|
||||
## Trigger
|
||||
`/research-add-items`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Auto-locate Outline
|
||||
Find `*/outline.yaml` file in current working directory, auto-read.
|
||||
|
||||
### Step 2: Get Supplement Sources in Parallel
|
||||
Simultaneously:
|
||||
- **A. Ask user**: What items to supplement? Any specific names?
|
||||
- **B. Ask if Web Search needed**: Launch agent to search for more items?
|
||||
|
||||
### Step 3: Merge and Update
|
||||
- Append new items to outline.yaml
|
||||
- Display to user for confirmation
|
||||
- Avoid duplicates
|
||||
- Save updated outline
|
||||
|
||||
## Output
|
||||
Updated `{topic}/outline.yaml` file (in-place modification)
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
user-invocable: true
|
||||
description: Read research outline, launch independent agent for each item for deep research. Disable task output.
|
||||
allowed-tools: Bash, Read, Write, Glob, WebSearch, Task
|
||||
---
|
||||
|
||||
# Research Deep - Deep Research
|
||||
|
||||
## Trigger
|
||||
`/research-deep`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Auto-locate Outline
|
||||
Find `*/outline.yaml` file in current working directory, read items list, execution config (including items_per_agent).
|
||||
|
||||
### Step 2: Resume Check
|
||||
- Check completed JSON files in output_dir
|
||||
- Skip completed items
|
||||
|
||||
### Step 3: Batch Execution
|
||||
- Batch by batch_size (need user approval before next batch)
|
||||
- Each agent handles items_per_agent items
|
||||
- Launch web-search-agent (background parallel, disable task output)
|
||||
|
||||
**Parameter Retrieval**:
|
||||
- `{topic}`: topic field from outline.yaml
|
||||
- `{item_name}`: item's name field
|
||||
- `{item_related_info}`: item's complete yaml content (name + category + description etc.)
|
||||
- `{output_dir}`: execution.output_dir from outline.yaml (default: ./results)
|
||||
- `{fields_path}`: absolute path to {topic}/fields.yaml
|
||||
- `{output_path}`: absolute path to {output_dir}/{item_name_slug}.json (slugify item_name: replace spaces with _, remove special chars)
|
||||
|
||||
**Hard Constraint**: The following prompt must be strictly reproduced, only replacing variables in {xxx}, do not modify structure or wording.
|
||||
|
||||
**Prompt Template**:
|
||||
```python
|
||||
prompt = f"""## Task
|
||||
Research {item_related_info}, output structured JSON to {output_path}
|
||||
|
||||
## Field Definitions
|
||||
Read {fields_path} to get all field definitions
|
||||
|
||||
## Output Requirements
|
||||
1. Output JSON according to fields defined in fields.yaml
|
||||
2. Mark uncertain field values with [uncertain]
|
||||
3. Add uncertain array at the end of JSON, listing all uncertain field names
|
||||
4. All field values must be in English
|
||||
|
||||
## Output Path
|
||||
{output_path}
|
||||
|
||||
## Validation
|
||||
After completing JSON output, run validation script to ensure complete field coverage:
|
||||
python ~/.claude/skills/research/validate_json.py -f {fields_path} -j {output_path}
|
||||
Task is complete only after validation passes.
|
||||
"""
|
||||
```
|
||||
|
||||
**One-shot Example** (assuming researching GitHub Copilot):
|
||||
```
|
||||
## Task
|
||||
Research name: GitHub Copilot
|
||||
category: International Product
|
||||
description: Developed by Microsoft/GitHub, first mainstream AI coding assistant, ~40% market share, output structured JSON to /home/weizhena/AIcoding/aicoding-history/results/GitHub_Copilot.json
|
||||
|
||||
## Field Definitions
|
||||
Read /home/weizhena/AIcoding/aicoding-history/fields.yaml to get all field definitions
|
||||
|
||||
## Output Requirements
|
||||
1. Output JSON according to fields defined in fields.yaml
|
||||
2. Mark uncertain field values with [uncertain]
|
||||
3. Add uncertain array at the end of JSON, listing all uncertain field names
|
||||
4. All field values must be in English
|
||||
|
||||
## Output Path
|
||||
/home/weizhena/AIcoding/aicoding-history/results/GitHub_Copilot.json
|
||||
|
||||
## Validation
|
||||
After completing JSON output, run validation script to ensure complete field coverage:
|
||||
python ~/.claude/skills/research/validate_json.py -f /home/weizhena/AIcoding/aicoding-history/fields.yaml -j /home/weizhena/AIcoding/aicoding-history/results/GitHub_Copilot.json
|
||||
Task is complete only after validation passes.
|
||||
```
|
||||
|
||||
### Step 4: Wait and Monitor
|
||||
- Wait for current batch to complete
|
||||
- Launch next batch
|
||||
- Display progress
|
||||
|
||||
### Step 5: Summary Report
|
||||
After all complete, output:
|
||||
- Completion count
|
||||
- Failed/uncertain marked items
|
||||
- Output directory
|
||||
|
||||
## Agent Config
|
||||
- Background execution: Yes
|
||||
- Task Output: Disabled (agent has explicit output file when complete)
|
||||
- Resume support: Yes
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
user-invocable: true
|
||||
description: Summarize deep research results into markdown report, cover all fields, skip uncertain values.
|
||||
allowed-tools: Read, Write, Glob, Bash, AskUserQuestion
|
||||
---
|
||||
|
||||
# Research Report - Summary Report
|
||||
|
||||
## Trigger
|
||||
`/research-report`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Locate Results Directory
|
||||
Find `*/outline.yaml` in current working directory, read topic and output_dir config.
|
||||
|
||||
### Step 2: Scan Optional Summary Fields
|
||||
Read all JSON results, extract fields suitable for TOC display (numeric, short metrics), e.g.:
|
||||
- github_stars
|
||||
- google_scholar_cites
|
||||
- swe_bench_score
|
||||
- user_scale
|
||||
- valuation
|
||||
- release_date
|
||||
|
||||
Use AskUserQuestion to ask user:
|
||||
- Which fields to display in TOC besides item name?
|
||||
- Provide dynamic options list (based on actual fields in JSON)
|
||||
|
||||
### Step 3: Generate Python Conversion Script
|
||||
Generate `generate_report.py` in `{topic}/` directory, script requirements:
|
||||
- Read all JSON from output_dir
|
||||
- Read fields.yaml to get field structure
|
||||
- Cover all field values from each JSON
|
||||
- Skip fields with values containing [uncertain]
|
||||
- Skip fields listed in uncertain array
|
||||
- Generate markdown report format: Table of contents (with anchor links + user-selected summary fields) + Detailed content (by field category)
|
||||
- Save to `{topic}/report.md`
|
||||
|
||||
**TOC Format Requirements**:
|
||||
- Must include every item
|
||||
- Each item displays: number, name (anchor link), user-selected summary fields
|
||||
- Example: `1. [GitHub Copilot](#github-copilot) - Stars: 10k | Score: 85%`
|
||||
|
||||
#### Script Technical Requirements (Must Follow)
|
||||
|
||||
**1. JSON Structure Compatibility**
|
||||
Support two JSON structures:
|
||||
- Flat structure: Fields directly at top level `{"name": "xxx", "release_date": "xxx"}`
|
||||
- Nested structure: Fields in category sub-dict `{"basic_info": {"name": "xxx"}, "technical_features": {...}}`
|
||||
|
||||
Field lookup order: Top level -> category mapping key -> Traverse all nested dicts
|
||||
|
||||
**2. Category Multi-language Mapping**
|
||||
fields.yaml category names and JSON keys can be any combination (CN-CN, CN-EN, EN-CN, EN-EN). Must establish bidirectional mapping:
|
||||
```python
|
||||
CATEGORY_MAPPING = {
|
||||
"Basic Info": ["basic_info", "Basic Info"],
|
||||
"Technical Features": ["technical_features", "technical_characteristics", "Technical Features"],
|
||||
"Performance Metrics": ["performance_metrics", "performance", "Performance Metrics"],
|
||||
"Milestone Significance": ["milestone_significance", "milestones", "Milestone Significance"],
|
||||
"Business Info": ["business_info", "commercial_info", "Business Info"],
|
||||
"Competition & Ecosystem": ["competition_ecosystem", "competition", "Competition & Ecosystem"],
|
||||
"History": ["history", "History"],
|
||||
"Market Positioning": ["market_positioning", "market", "Market Positioning"],
|
||||
}
|
||||
```
|
||||
|
||||
**3. Complex Value Formatting**
|
||||
- list of dicts (e.g., key_events, funding_history): Format each dict as one line, separate kv with ` | `
|
||||
- Normal list: Short lists joined with comma, long lists displayed with line breaks
|
||||
- Nested dict: Recursive formatting, display with semicolon or line breaks
|
||||
- Long text strings (over 100 chars): Add line breaks `<br>` or use blockquote format for readability
|
||||
|
||||
**4. Extra Fields Collection**
|
||||
Collect fields that exist in JSON but not defined in fields.yaml, put in "Other Info" category. Note to filter:
|
||||
- Internal fields: `_source_file`, `uncertain`
|
||||
- Nested structure top-level keys: `basic_info`, `technical_features` etc.
|
||||
- `uncertain` array: Display each field name on separate line, don't compress into one line
|
||||
|
||||
**5. Uncertain Value Skipping**
|
||||
Skip conditions:
|
||||
- Field value contains `[uncertain]` string
|
||||
- Field name is in `uncertain` array
|
||||
- Field value is None or empty string
|
||||
|
||||
### Step 4: Execute Script
|
||||
Run `python {topic}/generate_report.py`
|
||||
|
||||
## Output
|
||||
- `{topic}/generate_report.py` - Conversion script
|
||||
- `{topic}/report.md` - Summary report
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
user-invocable: true
|
||||
allowed-tools: Read, Write, Glob, WebSearch, Task, AskUserQuestion
|
||||
description: Conduct preliminary research on a topic and generate research outline. For academic research, benchmark research, technology selection, etc.
|
||||
---
|
||||
|
||||
# Research Skill - Preliminary Research
|
||||
|
||||
## Trigger
|
||||
`/research <topic>`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Generate Initial Framework from Model Knowledge
|
||||
Based on topic, use model's existing knowledge to generate:
|
||||
- Main research objects/items list in this domain
|
||||
- Suggested research field framework
|
||||
|
||||
Output {step1_output}, use AskUserQuestion to confirm:
|
||||
- Need to add/remove items?
|
||||
- Does field framework meet requirements?
|
||||
|
||||
### Step 2: Web Search Supplement
|
||||
Use AskUserQuestion to ask for time range (e.g., last 6 months, since 2024, unlimited).
|
||||
|
||||
**Parameter Retrieval**:
|
||||
- `{topic}`: User input research topic
|
||||
- `{YYYY-MM-DD}`: Current date
|
||||
- `{step1_output}`: Complete output from Step 1
|
||||
- `{time_range}`: User specified time range
|
||||
|
||||
**Hard Constraint**: The following prompt must be strictly reproduced, only replacing variables in {xxx}, do not modify structure or wording.
|
||||
|
||||
Launch 1 web-search-agent (background), **Prompt Template**:
|
||||
```python
|
||||
prompt = f"""## Task
|
||||
Research topic: {topic}
|
||||
Current date: {YYYY-MM-DD}
|
||||
|
||||
Based on the following initial framework, supplement latest items and recommended research fields.
|
||||
|
||||
## Existing Framework
|
||||
{step1_output}
|
||||
|
||||
## Goals
|
||||
1. Verify if existing items are missing important objects
|
||||
2. Supplement items based on missing objects
|
||||
3. Continue searching for {topic} related items within {time_range} and supplement
|
||||
4. Supplement new fields
|
||||
|
||||
## Output Requirements
|
||||
Return structured results directly (do not write files):
|
||||
|
||||
### Supplementary Items
|
||||
- item_name: Brief explanation (why it should be added)
|
||||
...
|
||||
|
||||
### Recommended Supplementary Fields
|
||||
- field_name: Field description (why this dimension is needed)
|
||||
...
|
||||
|
||||
### Sources
|
||||
- [Source1](url1)
|
||||
- [Source2](url2)
|
||||
"""
|
||||
```
|
||||
|
||||
**One-shot Example** (assuming researching AI Coding History):
|
||||
```
|
||||
## Task
|
||||
Research topic: AI Coding History
|
||||
Current date: 2025-12-30
|
||||
|
||||
Based on the following initial framework, supplement latest items and recommended research fields.
|
||||
|
||||
## Existing Framework
|
||||
### Items List
|
||||
1. GitHub Copilot: Developed by Microsoft/GitHub, first mainstream AI coding assistant
|
||||
2. Cursor: AI-first IDE, based on VSCode
|
||||
...
|
||||
|
||||
### Field Framework
|
||||
- Basic Info: name, release_date, company
|
||||
- Technical Features: underlying_model, context_window
|
||||
...
|
||||
|
||||
## Goals
|
||||
1. Verify if existing items are missing important objects
|
||||
2. Supplement items based on missing objects
|
||||
3. Continue searching for AI Coding History related items within since 2024 and supplement
|
||||
4. Supplement new fields
|
||||
|
||||
## Output Requirements
|
||||
Return structured results directly (do not write files):
|
||||
|
||||
### Supplementary Items
|
||||
- item_name: Brief explanation (why it should be added)
|
||||
...
|
||||
|
||||
### Recommended Supplementary Fields
|
||||
- field_name: Field description (why this dimension is needed)
|
||||
...
|
||||
|
||||
### Sources
|
||||
- [Source1](url1)
|
||||
- [Source2](url2)
|
||||
```
|
||||
|
||||
### Step 3: Ask User for Existing Fields
|
||||
Use AskUserQuestion to ask if user has existing field definition file, if so read and merge.
|
||||
|
||||
### Step 4: Generate Outline (Separate Files)
|
||||
Merge {step1_output}, {step2_output} and user's existing fields, generate two files:
|
||||
|
||||
**outline.yaml** (items + config):
|
||||
- topic: Research topic
|
||||
- items: Research objects list
|
||||
- execution:
|
||||
- batch_size: Number of parallel agents (confirm with AskUserQuestion)
|
||||
- items_per_agent: Items per agent (confirm with AskUserQuestion)
|
||||
- output_dir: Results output directory (default: ./results)
|
||||
|
||||
**fields.yaml** (field definitions):
|
||||
- Field categories and definitions
|
||||
- Each field's name, description, detail_level
|
||||
- detail_level hierarchy: brief -> moderate -> detailed
|
||||
- uncertain: Uncertain fields list (reserved field, auto-filled in deep phase)
|
||||
|
||||
### Step 5: Output and Confirm
|
||||
- Create directory: `./{topic_slug}/`
|
||||
- Save: `outline.yaml` and `fields.yaml`
|
||||
- Show to user for confirmation
|
||||
|
||||
## Output Path
|
||||
```
|
||||
{current_working_directory}/{topic_slug}/
|
||||
├── outline.yaml # items list + execution config
|
||||
└── fields.yaml # field definitions
|
||||
```
|
||||
|
||||
## Follow-up Commands
|
||||
- `/research-add-items` - Supplement items
|
||||
- `/research-add-fields` - Supplement fields
|
||||
- `/research-deep` - Start deep research
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
CATEGORY_MAPPING = {
|
||||
"basic_info": ["basic_info", "Basic Info"],
|
||||
"technical_features": ["technical_features", "technical_characteristics", "Technical Features"],
|
||||
"performance_metrics": ["performance_metrics", "performance", "Performance Metrics"],
|
||||
"milestone_significance": ["milestone_significance", "milestones", "Milestone Significance"],
|
||||
"business_info": ["business_info", "commercial_info", "Business Info"],
|
||||
"competition_ecosystem": ["competition_ecosystem", "competition", "Competition Ecosystem"],
|
||||
"history": ["history", "History"],
|
||||
"market_positioning": ["market_positioning", "market", "Market Positioning"],
|
||||
}
|
||||
|
||||
_SKIP_KEYS = {"_source_file", "uncertain"}
|
||||
|
||||
|
||||
def load_fields_yaml(fields_path):
|
||||
with fields_path.open(encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
items = [
|
||||
(field["name"], category["category"], field.get("required", False))
|
||||
for category in data.get("field_categories", [])
|
||||
for field in category.get("fields", [])
|
||||
]
|
||||
all_fields = {name for name, _, _ in items}
|
||||
required_fields = {name for name, _, required in items if required}
|
||||
field_categories = {name: category for name, category, _ in items}
|
||||
return all_fields, required_fields, field_categories
|
||||
|
||||
|
||||
def extract_json_fields(data, category_mapping=None):
|
||||
category_mapping = CATEGORY_MAPPING if category_mapping is None else category_mapping
|
||||
nested_keys = {k for keys in category_mapping.values() for k in keys}
|
||||
fields = set()
|
||||
stack = [(data, True)]
|
||||
while stack:
|
||||
obj, is_category_level = stack.pop()
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
if k in _SKIP_KEYS:
|
||||
continue
|
||||
if is_category_level and k in nested_keys:
|
||||
if isinstance(v, dict):
|
||||
stack.append((v, True))
|
||||
continue
|
||||
fields.add(k)
|
||||
elif isinstance(obj, list):
|
||||
stack.extend((item, is_category_level) for item in obj if isinstance(item, dict))
|
||||
return fields
|
||||
|
||||
|
||||
def validate_json(json_path, all_fields, required_fields, field_categories):
|
||||
with json_path.open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
json_fields = extract_json_fields(data)
|
||||
covered = all_fields & json_fields
|
||||
missing = all_fields - json_fields
|
||||
extra = json_fields - all_fields
|
||||
missing_required = missing & required_fields
|
||||
missing_by_category = defaultdict(list)
|
||||
for field in missing:
|
||||
missing_by_category[field_categories.get(field, "Unknown")].append(field)
|
||||
return {
|
||||
"file": json_path.name,
|
||||
"total_defined": len(all_fields),
|
||||
"covered": len(covered),
|
||||
"missing": len(missing),
|
||||
"extra": len(extra),
|
||||
"coverage_rate": len(covered) / len(all_fields) * 100 if all_fields else 100,
|
||||
"missing_required": sorted(missing_required),
|
||||
"missing_optional": sorted(missing - required_fields),
|
||||
"missing_by_category": {k: sorted(v) for k, v in missing_by_category.items()},
|
||||
"extra_fields": sorted(extra),
|
||||
"valid": len(missing_required) == 0,
|
||||
}
|
||||
|
||||
|
||||
def print_result(result, verbose=True):
|
||||
status = "PASS" if result["valid"] else "FAIL"
|
||||
line = "=" * 60
|
||||
print(f"\n{line}")
|
||||
print(f"[{status}] {result['file']}")
|
||||
print(line)
|
||||
print(f"Coverage: {result['coverage_rate']:.1f}% ({result['covered']}/{result['total_defined']})")
|
||||
if result["missing_required"]:
|
||||
print(f"\n[ERROR] Missing required fields ({len(result['missing_required'])}):")
|
||||
print("\n".join(f" - {f}" for f in result["missing_required"]))
|
||||
if verbose and result["missing_optional"]:
|
||||
missing_required = set(result["missing_required"])
|
||||
print(f"\n[WARN] Missing optional fields ({len(result['missing_optional'])}):")
|
||||
for cat in sorted(result["missing_by_category"]):
|
||||
optional = [f for f in result["missing_by_category"][cat] if f not in missing_required]
|
||||
if optional:
|
||||
print(f" [{cat}]: {', '.join(optional)}")
|
||||
if verbose and result["extra_fields"]:
|
||||
extra = result["extra_fields"]
|
||||
print(f"\n[INFO] Extra fields ({len(extra)}):")
|
||||
print(f" {', '.join(extra[:10])}")
|
||||
if len(extra) > 10:
|
||||
print(f" ... and {len(extra) - 10} more")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Validate whether JSON files cover all fields defined in fields.yaml")
|
||||
parser.add_argument("--fields", "-f", type=str, help="Path to fields.yaml", default="fields.yaml")
|
||||
parser.add_argument("--json", "-j", type=str, nargs="*", help="JSON file paths to validate")
|
||||
parser.add_argument("--dir", "-d", type=str, help="Directory containing JSON files", default="results")
|
||||
parser.add_argument("--quiet", "-q", action="store_true", help="Show summary only")
|
||||
args = parser.parse_args()
|
||||
fields_path = Path(args.fields)
|
||||
if not fields_path.exists():
|
||||
for p in (Path.cwd() / "fields.yaml", Path.cwd().parent / "fields.yaml"):
|
||||
if p.exists():
|
||||
fields_path = p
|
||||
break
|
||||
if not fields_path.exists():
|
||||
print(f"[ERROR] fields.yaml not found: {fields_path}")
|
||||
sys.exit(1)
|
||||
print(f"Field definition file: {fields_path}")
|
||||
all_fields, required_fields, field_categories = load_fields_yaml(fields_path)
|
||||
print(f"Total fields: {len(all_fields)} (required: {len(required_fields)}, optional: {len(all_fields) - len(required_fields)})")
|
||||
json_files = (
|
||||
[Path(p) for p in args.json]
|
||||
if args.json
|
||||
else sorted(Path(args.dir).glob("*.json")) if Path(args.dir).exists() else []
|
||||
)
|
||||
if not json_files:
|
||||
print("[WARN] No JSON files found")
|
||||
sys.exit(0)
|
||||
results = []
|
||||
for json_path in json_files:
|
||||
if not json_path.exists():
|
||||
print(f"[WARN] File not found: {json_path}")
|
||||
continue
|
||||
result = validate_json(json_path, all_fields, required_fields, field_categories)
|
||||
results.append(result)
|
||||
print_result(result, verbose=not args.quiet)
|
||||
line = "=" * 60
|
||||
print(f"\n{line}")
|
||||
print("Summary")
|
||||
print(line)
|
||||
passed = sum(1 for r in results if r["valid"])
|
||||
avg_coverage = sum(r["coverage_rate"] for r in results) / len(results) if results else 0
|
||||
print(f"Validation passed: {passed}/{len(results)}")
|
||||
print(f"Average coverage: {avg_coverage:.1f}%")
|
||||
if passed < len(results):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
user-invocable: true
|
||||
description: 向现有调研outline补充字段定义。
|
||||
allowed-tools: Bash, Read, Write, Glob, WebSearch, Task, AskUserQuestion
|
||||
---
|
||||
|
||||
# Research Add Fields - 补充调研字段
|
||||
|
||||
## 触发方式
|
||||
`/research-add-fields`
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Step 1: 自动定位Fields文件
|
||||
在当前工作目录查找 `*/fields.yaml` 文件,自动读取现有fields定义。
|
||||
|
||||
### Step 2: 获取补充来源
|
||||
询问用户选择:
|
||||
- **A. 用户直接输入**:用户提供字段名称和描述
|
||||
- **B. Web Search搜索**:启动web-search-agent搜索该领域常用字段
|
||||
|
||||
### Step 3: 展示并确认
|
||||
- 展示建议的新字段列表
|
||||
- 用户确认哪些字段需要添加
|
||||
- 用户指定字段分类和detail_level
|
||||
|
||||
### Step 4: 保存更新
|
||||
将确认的字段追加到fields.yaml,保存文件。
|
||||
|
||||
## 输出
|
||||
更新后的 `{topic}/fields.yaml` 文件(原地修改,需用户确认)
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
user-invocable: true
|
||||
description: 向现有调研outline补充items(调研对象)。
|
||||
allowed-tools: Bash, Read, Write, Glob, WebSearch, Task, AskUserQuestion
|
||||
---
|
||||
|
||||
# Research Add Items - 补充调研对象
|
||||
|
||||
## 触发方式
|
||||
`/research-add-items`
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Step 1: 自动定位Outline
|
||||
在当前工作目录查找 `*/outline.yaml` 文件,自动读取。
|
||||
|
||||
### Step 2: 并行获取补充来源
|
||||
同时进行:
|
||||
- **A. 询问用户**:需要补充哪些items?有具体名称吗?
|
||||
- **B. 询问是否需要Web Search**:是否启动agent搜索更多items?
|
||||
|
||||
### Step 3: 合并更新
|
||||
- 将新items追加到outline.yaml
|
||||
- 展示给用户确认
|
||||
- 避免重复
|
||||
- 保存更新后的outline
|
||||
|
||||
## 输出
|
||||
更新后的 `{topic}/outline.yaml` 文件(原地修改)
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
user-invocable: true
|
||||
description: 读取调研outline,为每个item启动独立agent进行深度调研。禁用task output。
|
||||
allowed-tools: Bash, Read, Write, Glob, WebSearch, Task
|
||||
---
|
||||
|
||||
# Research Deep - 深度调研
|
||||
|
||||
## 触发方式
|
||||
`/research-deep`
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Step 1: 自动定位Outline
|
||||
在当前工作目录查找 `*/outline.yaml` 文件,读取items列表、execution配置(含items_per_agent)。
|
||||
|
||||
### Step 2: 断点续传检查
|
||||
- 检查output_dir下已完成的JSON文件
|
||||
- 跳过已完成的items
|
||||
|
||||
### Step 3: 分批执行
|
||||
- 按batch_size分批(完成一批需要得到用户同意才可进行下一批)
|
||||
- 每个agent负责items_per_agent个项目
|
||||
- 启动web-search-agent(后台并行,禁用task output)
|
||||
|
||||
**参数获取**:
|
||||
- `{topic}`: outline.yaml中的topic字段
|
||||
- `{item_name}`: item的name字段
|
||||
- `{item_related_info}`: item的完整yaml内容(name + category + description等)
|
||||
- `{output_dir}`: outline.yaml中execution.output_dir(默认./results)
|
||||
- `{fields_path}`: {topic}/fields.yaml的绝对路径
|
||||
- `{output_path}`: {output_dir}/{item_name_slug}.json的绝对路径(slugify处理item_name:空格替换为_,移除特殊字符)
|
||||
|
||||
**硬约束**:以下prompt必须严格复述,仅替换{xxx}中的变量,禁止改写结构或措辞。
|
||||
|
||||
**Prompt模板**:
|
||||
```python
|
||||
prompt = f"""## 任务
|
||||
调研 {item_related_info},输出结构化JSON到 {output_path}
|
||||
|
||||
## 字段定义
|
||||
读取 {fields_path} 获取所有字段定义
|
||||
|
||||
## 输出要求
|
||||
1. 按fields.yaml定义的字段输出JSON
|
||||
2. 不确定的字段值标注[不确定]
|
||||
3. JSON末尾添加uncertain数组,列出所有不确定的字段名
|
||||
4. 所有字段值必须使用中文输出(调研过程可用英文,但最终JSON值为中文)
|
||||
|
||||
## 输出路径
|
||||
{output_path}
|
||||
|
||||
## 验证
|
||||
完成JSON输出后,运行验证脚本确保字段完整覆盖:
|
||||
python ~/.claude/skills/research/validate_json.py -f {fields_path} -j {output_path}
|
||||
验证通过后才算完成任务。
|
||||
"""
|
||||
```
|
||||
|
||||
**One-shot示例**(假设调研GitHub Copilot):
|
||||
```
|
||||
## 任务
|
||||
调研 name: GitHub Copilot
|
||||
category: 国际产品
|
||||
description: Microsoft/GitHub开发,首个主流AI编程助手,市场份额约40%,输出结构化JSON到 /home/weizhena/AIcoding/aicoding-history/results/GitHub_Copilot.json
|
||||
|
||||
## 字段定义
|
||||
读取 /home/weizhena/AIcoding/aicoding-history/fields.yaml 获取所有字段定义
|
||||
|
||||
## 输出要求
|
||||
1. 按fields.yaml定义的字段输出JSON
|
||||
2. 不确定的字段值标注[不确定]
|
||||
3. JSON末尾添加uncertain数组,列出所有不确定的字段名
|
||||
4. 所有字段值必须使用中文输出(调研过程可用英文,但最终JSON值为中文)
|
||||
|
||||
## 输出路径
|
||||
/home/weizhena/AIcoding/aicoding-history/results/GitHub_Copilot.json
|
||||
|
||||
## 验证
|
||||
完成JSON输出后,运行验证脚本确保字段完整覆盖:
|
||||
python ~/.claude/skills/research/validate_json.py -f /home/weizhena/AIcoding/aicoding-history/fields.yaml -j /home/weizhena/AIcoding/aicoding-history/results/GitHub_Copilot.json
|
||||
验证通过后才算完成任务。
|
||||
```
|
||||
|
||||
### Step 4: 等待与监控
|
||||
- 等待当前批次完成
|
||||
- 启动下一批
|
||||
- 显示进度
|
||||
|
||||
### Step 5: 汇总报告
|
||||
全部完成后输出:
|
||||
- 完成数量
|
||||
- 失败/不确定标记的items
|
||||
- 输出目录
|
||||
|
||||
## Agent配置
|
||||
- 后台执行: 是
|
||||
- Task Output: 禁用(agent完成时有明确输出文件)
|
||||
- 断点续传: 是
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
user-invocable: true
|
||||
description: 将deep调研结果汇总为markdown报告,覆盖所有字段,跳过不确定值。
|
||||
allowed-tools: Read, Write, Glob, Bash, AskUserQuestion
|
||||
---
|
||||
|
||||
# Research Report - 汇总报告
|
||||
|
||||
## 触发方式
|
||||
`/research-report`
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Step 1: 定位结果目录
|
||||
在当前工作目录查找 `*/outline.yaml`,读取topic和output_dir配置。
|
||||
|
||||
### Step 2: 扫描可选摘要字段
|
||||
读取所有JSON结果,提取适合在目录中显示的字段(数值型、简短指标),例如:
|
||||
- github_stars
|
||||
- google_scholar_cites
|
||||
- swe_bench_score
|
||||
- user_scale
|
||||
- valuation
|
||||
- release_date
|
||||
|
||||
使用AskUserQuestion询问用户:
|
||||
- 目录中除了item名称外,还需要显示哪些字段?
|
||||
- 提供动态选项列表(基于实际JSON中存在的字段)
|
||||
|
||||
### Step 3: 生成Python转换脚本
|
||||
在 `{topic}/` 目录下生成 `generate_report.py`,脚本要求:
|
||||
- 读取output_dir下所有JSON
|
||||
- 读取fields.yaml获取字段结构
|
||||
- 覆盖每个JSON的所有字段值
|
||||
- 跳过值包含[不确定]的字段
|
||||
- 跳过uncertain数组中列出的字段
|
||||
- 生成markdown报告格式:目录(带锚点跳转+用户选择的摘要字段)+ 详细内容(按字段分类)
|
||||
- 保存到 `{topic}/report.md`
|
||||
|
||||
**目录格式要求**:
|
||||
- 必须包含每一个item
|
||||
- 每个item显示:序号、名称(锚点链接)、用户选择的摘要字段
|
||||
- 示例:`1. [GitHub Copilot](#github-copilot) - Stars: 10k | Score: 85%`
|
||||
|
||||
#### 脚本技术要点(必须遵循)
|
||||
|
||||
**1. JSON结构兼容**
|
||||
支持两种JSON结构:
|
||||
- 扁平结构:字段直接在顶层 `{"name": "xxx", "release_date": "xxx"}`
|
||||
- 嵌套结构:字段在category子dict中 `{"basic_info": {"name": "xxx"}, "technical_features": {...}}`
|
||||
|
||||
字段查找顺序:顶层 -> category映射key -> 遍历所有嵌套dict
|
||||
|
||||
**2. Category多语言映射**
|
||||
fields.yaml的category名与JSON的key可能是任意组合(中中、中英、英中、英英)。必须建立双向映射:
|
||||
```python
|
||||
CATEGORY_MAPPING = {
|
||||
"基本信息": ["basic_info", "基本信息"],
|
||||
"技术特性": ["technical_features", "technical_characteristics", "技术特性"],
|
||||
"性能指标": ["performance_metrics", "performance", "性能指标"],
|
||||
"里程碑意义": ["milestone_significance", "milestones", "里程碑意义"],
|
||||
"商业信息": ["business_info", "commercial_info", "商业信息"],
|
||||
"竞争与生态": ["competition_ecosystem", "competition", "竞争与生态"],
|
||||
"历史沿革": ["history", "历史沿革"],
|
||||
"市场定位": ["market_positioning", "market", "市场定位"],
|
||||
}
|
||||
```
|
||||
|
||||
**3. 复杂值格式化**
|
||||
- list of dicts(如key_events, funding_history):每个dict格式化为一行,用` | `分隔kv
|
||||
- 普通list:短列表用逗号连接,长列表换行显示
|
||||
- 嵌套dict:递归格式化,用分号或换行显示
|
||||
- 长文本字符串(超过100字符):添加换行符`<br>`或使用blockquote格式,提高可读性
|
||||
|
||||
**4. 额外字段收集**
|
||||
收集JSON中有但fields.yaml中没定义的字段,放入"其他信息"分类。注意过滤:
|
||||
- 内部字段:`_source_file`, `uncertain`
|
||||
- 嵌套结构顶级key:`basic_info`, `technical_features`等
|
||||
- `uncertain`数组:需要逐行显示每个字段名,不要压缩成一行
|
||||
|
||||
**5. 不确定值跳过**
|
||||
跳过条件:
|
||||
- 字段值包含`[不确定]`字符串
|
||||
- 字段名在`uncertain`数组中
|
||||
- 字段值为None或空字符串
|
||||
|
||||
### Step 4: 执行脚本
|
||||
运行 `python {topic}/generate_report.py`
|
||||
|
||||
## 输出
|
||||
- `{topic}/generate_report.py` - 转换脚本
|
||||
- `{topic}/report.md` - 汇总报告
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
user-invocable: true
|
||||
allowed-tools: Read, Write, Glob, WebSearch, Task, AskUserQuestion
|
||||
description: 对目标话题进行初步调研,生成调研outline。用于学术调研、benchmark调研、技术选型等场景。
|
||||
---
|
||||
|
||||
# Research Skill - 初步调研
|
||||
|
||||
## 触发方式
|
||||
`/research <topic>`
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Step 1: 模型内部知识生成初步框架
|
||||
基于topic,利用模型已有知识生成:
|
||||
- 该领域的主要研究对象/items列表
|
||||
- 建议的调研字段框架
|
||||
|
||||
输出{step1_output},使用AskUserQuestion确认:
|
||||
- items列表是否需要增减?
|
||||
- 字段框架是否满足需求?
|
||||
|
||||
### Step 2: Web Search补充
|
||||
使用AskUserQuestion询问时间范围(如:最近6个月、2024年至今、不限)。
|
||||
|
||||
**参数获取**:
|
||||
- `{topic}`: 用户输入的调研话题
|
||||
- `{YYYY-MM-DD}`: 当前日期
|
||||
- `{step1_output}`: Step 1生成的完整输出内容
|
||||
- `{time_range}`: 用户指定的时间范围
|
||||
|
||||
**硬约束**:以下prompt必须严格复述,仅替换{xxx}中的变量,禁止改写结构或措辞。
|
||||
|
||||
启动1个web-search-agent(后台),**Prompt模板**:
|
||||
```python
|
||||
prompt = f"""## 任务
|
||||
调研话题: {topic}
|
||||
当前日期: {YYYY-MM-DD}
|
||||
|
||||
基于以下初步框架,补充最新items和推荐调研字段。
|
||||
|
||||
## 已有框架
|
||||
{step1_output}
|
||||
|
||||
## 目标
|
||||
1. 验证已有items是否遗漏重要对象
|
||||
2. 根据遗漏对象进行补充items
|
||||
3. 继续搜索{topic}相关且{time_range}内的items并补充
|
||||
4. 补充新fields
|
||||
|
||||
## 输出要求
|
||||
直接返回结构化结果(不写文件):
|
||||
|
||||
### 补充Items
|
||||
- item_name: 简要说明(为什么应该加入)
|
||||
...
|
||||
|
||||
### 推荐补充字段
|
||||
- field_name: 字段描述(为什么需要这个维度)
|
||||
...
|
||||
|
||||
### 信息来源
|
||||
- [来源1](url1)
|
||||
- [来源2](url2)
|
||||
"""
|
||||
```
|
||||
|
||||
**One-shot示例**(假设调研AI Coding发展史):
|
||||
```
|
||||
## 任务
|
||||
调研话题: AI Coding 发展史
|
||||
当前日期: 2025-12-30
|
||||
|
||||
基于以下初步框架,补充最新items和推荐调研字段。
|
||||
|
||||
## 已有框架
|
||||
### Items列表
|
||||
1. GitHub Copilot: Microsoft/GitHub开发,首个主流AI编程助手
|
||||
2. Cursor: AI-first IDE,基于VSCode
|
||||
...
|
||||
|
||||
### 字段框架
|
||||
- 基本信息: name, release_date, company
|
||||
- 技术特性: underlying_model, context_window
|
||||
...
|
||||
|
||||
## 目标
|
||||
1. 验证已有items是否遗漏重要对象
|
||||
2. 根据遗漏对象进行补充items
|
||||
3. 继续搜索AI Coding 发展史相关且2024年至今内的items并补充
|
||||
4. 补充新fields
|
||||
|
||||
## 输出要求
|
||||
直接返回结构化结果(不写文件):
|
||||
|
||||
### 补充Items
|
||||
- item_name: 简要说明(为什么应该加入)
|
||||
...
|
||||
|
||||
### 推荐补充字段
|
||||
- field_name: 字段描述(为什么需要这个维度)
|
||||
...
|
||||
|
||||
### 信息来源
|
||||
- [来源1](url1)
|
||||
- [来源2](url2)
|
||||
```
|
||||
|
||||
### Step 3: 询问用户已有字段
|
||||
使用AskUserQuestion询问用户是否有已定义的字段文件,如有则读取并合并。
|
||||
|
||||
### Step 4: 生成Outline(分离文件)
|
||||
合并{step1_output}、{step2_output}和用户已有字段,生成两个文件:
|
||||
|
||||
**outline.yaml**(items + 配置):
|
||||
- topic: 调研主题
|
||||
- items: 调研对象列表
|
||||
- execution:
|
||||
- batch_size: 并行agent数量(需AskUserQuestion确认)
|
||||
- items_per_agent: 每个agent调研项目数(需AskUserQuestion确认)
|
||||
- output_dir: 结果输出目录(默认./results)
|
||||
|
||||
**fields.yaml**(字段定义):
|
||||
- 字段分类和定义
|
||||
- 每个字段的name、description、detail_level
|
||||
- detail_level分层:极简 → 简要 → 详细
|
||||
- uncertain: 不确定字段列表(保留字段,deep阶段自动填充)
|
||||
|
||||
### Step 5: 输出并确认
|
||||
- 创建目录: `./{topic_slug}/`
|
||||
- 保存: `outline.yaml` 和 `fields.yaml`
|
||||
- 展示给用户确认
|
||||
|
||||
## 输出路径
|
||||
```
|
||||
{当前工作目录}/{topic_slug}/
|
||||
├── outline.yaml # items列表 + execution配置
|
||||
└── fields.yaml # 字段定义
|
||||
```
|
||||
|
||||
## 后续命令
|
||||
- `/research-add-items` - 补充items
|
||||
- `/research-add-fields` - 补充字段
|
||||
- `/research-deep` - 开始深度调研
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
CATEGORY_MAPPING = {
|
||||
"基本信息": ["basic_info", "基本信息"],
|
||||
"技术特性": ["technical_features", "technical_characteristics", "技术特性"],
|
||||
"性能指标": ["performance_metrics", "performance", "性能指标"],
|
||||
"里程碑意义": ["milestone_significance", "milestones", "里程碑意义"],
|
||||
"商业信息": ["business_info", "commercial_info", "商业信息"],
|
||||
"竞争与生态": ["competition_ecosystem", "competition", "竞争与生态"],
|
||||
"历史沿革": ["history", "历史沿革"],
|
||||
"市场定位": ["market_positioning", "market", "市场定位"],
|
||||
}
|
||||
|
||||
_SKIP_KEYS = {"_source_file", "uncertain"}
|
||||
|
||||
|
||||
def load_fields_yaml(fields_path):
|
||||
with fields_path.open(encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
items = [
|
||||
(field["name"], category["category"], field.get("required", False))
|
||||
for category in data.get("field_categories", [])
|
||||
for field in category.get("fields", [])
|
||||
]
|
||||
all_fields = {name for name, _, _ in items}
|
||||
required_fields = {name for name, _, required in items if required}
|
||||
field_categories = {name: category for name, category, _ in items}
|
||||
return all_fields, required_fields, field_categories
|
||||
|
||||
|
||||
def extract_json_fields(data, category_mapping=None):
|
||||
category_mapping = CATEGORY_MAPPING if category_mapping is None else category_mapping
|
||||
nested_keys = {k for keys in category_mapping.values() for k in keys}
|
||||
fields = set()
|
||||
stack = [(data, True)]
|
||||
while stack:
|
||||
obj, is_category_level = stack.pop()
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
if k in _SKIP_KEYS:
|
||||
continue
|
||||
if is_category_level and k in nested_keys:
|
||||
if isinstance(v, dict):
|
||||
stack.append((v, True))
|
||||
continue
|
||||
fields.add(k)
|
||||
elif isinstance(obj, list):
|
||||
stack.extend((item, is_category_level) for item in obj if isinstance(item, dict))
|
||||
return fields
|
||||
|
||||
|
||||
def validate_json(json_path, all_fields, required_fields, field_categories):
|
||||
with json_path.open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
json_fields = extract_json_fields(data)
|
||||
covered = all_fields & json_fields
|
||||
missing = all_fields - json_fields
|
||||
extra = json_fields - all_fields
|
||||
missing_required = missing & required_fields
|
||||
missing_by_category = defaultdict(list)
|
||||
for field in missing:
|
||||
missing_by_category[field_categories.get(field, "未知")].append(field)
|
||||
return {
|
||||
"file": json_path.name,
|
||||
"total_defined": len(all_fields),
|
||||
"covered": len(covered),
|
||||
"missing": len(missing),
|
||||
"extra": len(extra),
|
||||
"coverage_rate": len(covered) / len(all_fields) * 100 if all_fields else 100,
|
||||
"missing_required": sorted(missing_required),
|
||||
"missing_optional": sorted(missing - required_fields),
|
||||
"missing_by_category": {k: sorted(v) for k, v in missing_by_category.items()},
|
||||
"extra_fields": sorted(extra),
|
||||
"valid": len(missing_required) == 0,
|
||||
}
|
||||
|
||||
|
||||
def print_result(result, verbose=True):
|
||||
status = "通过" if result["valid"] else "失败"
|
||||
line = "=" * 60
|
||||
print(f"\n{line}")
|
||||
print(f"[{status}] {result['file']}")
|
||||
print(line)
|
||||
print(f"覆盖率: {result['coverage_rate']:.1f}% ({result['covered']}/{result['total_defined']})")
|
||||
if result["missing_required"]:
|
||||
print(f"\n[错误] 缺少必填字段 ({len(result['missing_required'])}):")
|
||||
print("\n".join(f" - {f}" for f in result["missing_required"]))
|
||||
if verbose and result["missing_optional"]:
|
||||
missing_required = set(result["missing_required"])
|
||||
print(f"\n[警告] 缺少可选字段 ({len(result['missing_optional'])}):")
|
||||
for cat in sorted(result["missing_by_category"]):
|
||||
optional = [f for f in result["missing_by_category"][cat] if f not in missing_required]
|
||||
if optional:
|
||||
print(f" [{cat}]: {', '.join(optional)}")
|
||||
if verbose and result["extra_fields"]:
|
||||
extra = result["extra_fields"]
|
||||
print(f"\n[信息] 额外字段 ({len(extra)}):")
|
||||
print(f" {', '.join(extra[:10])}")
|
||||
if len(extra) > 10:
|
||||
print(f" ... 还有 {len(extra) - 10} 个")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="验证JSON文件是否覆盖fields.yaml中定义的所有字段")
|
||||
parser.add_argument("--fields", "-f", type=str, help="fields.yaml路径", default="fields.yaml")
|
||||
parser.add_argument("--json", "-j", type=str, nargs="*", help="要验证的JSON文件路径")
|
||||
parser.add_argument("--dir", "-d", type=str, help="包含JSON文件的目录", default="results")
|
||||
parser.add_argument("--quiet", "-q", action="store_true", help="仅显示摘要")
|
||||
args = parser.parse_args()
|
||||
fields_path = Path(args.fields)
|
||||
if not fields_path.exists():
|
||||
for p in (Path.cwd() / "fields.yaml", Path.cwd().parent / "fields.yaml"):
|
||||
if p.exists():
|
||||
fields_path = p
|
||||
break
|
||||
if not fields_path.exists():
|
||||
print(f"[错误] 找不到fields.yaml: {fields_path}")
|
||||
sys.exit(1)
|
||||
print(f"字段定义文件: {fields_path}")
|
||||
all_fields, required_fields, field_categories = load_fields_yaml(fields_path)
|
||||
print(f"总字段数: {len(all_fields)} (必填: {len(required_fields)}, 可选: {len(all_fields) - len(required_fields)})")
|
||||
json_files = (
|
||||
[Path(p) for p in args.json]
|
||||
if args.json
|
||||
else sorted(Path(args.dir).glob("*.json")) if Path(args.dir).exists() else []
|
||||
)
|
||||
if not json_files:
|
||||
print("[警告] 未找到JSON文件")
|
||||
sys.exit(0)
|
||||
results = []
|
||||
for json_path in json_files:
|
||||
if not json_path.exists():
|
||||
print(f"[警告] 文件不存在: {json_path}")
|
||||
continue
|
||||
result = validate_json(json_path, all_fields, required_fields, field_categories)
|
||||
results.append(result)
|
||||
print_result(result, verbose=not args.quiet)
|
||||
line = "=" * 60
|
||||
print(f"\n{line}")
|
||||
print("汇总")
|
||||
print(line)
|
||||
passed = sum(1 for r in results if r["valid"])
|
||||
avg_coverage = sum(r["coverage_rate"] for r in results) / len(results) if results else 0
|
||||
print(f"验证通过: {passed}/{len(results)}")
|
||||
print(f"平均覆盖率: {avg_coverage:.1f}%")
|
||||
if passed < len(results):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,3 +0,0 @@
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
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:
|
||||
@@ -1,127 +0,0 @@
|
||||
{
|
||||
"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-08T23:23:00Z",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "T1",
|
||||
"title": "Deduplicator compile fix",
|
||||
"tag": null,
|
||||
"status": "done",
|
||||
"startedAt": "2026-07-07T00:00:00Z",
|
||||
"finishedAt": null,
|
||||
"note": "BLOKER: build czerwony az to zrobione"
|
||||
},
|
||||
{
|
||||
"id": "T2",
|
||||
"title": "NameNormalizer testy",
|
||||
"tag": null,
|
||||
"status": "done",
|
||||
"startedAt": "2026-07-07T00:00:00Z",
|
||||
"finishedAt": "2026-07-07T00:00:00Z",
|
||||
"note": "makeSlug: NFKD+unicodeliterals+transliteratePolish; extractFullName: token-strip TITLE_PREFIXES (longest-first). 25 testowzielonych."
|
||||
},
|
||||
{
|
||||
"id": "T3",
|
||||
"title": "PartyNormalizer testy",
|
||||
"tag": null,
|
||||
"status": "done",
|
||||
"startedAt": "2026-07-07T00:00:00Z",
|
||||
"finishedAt": "2026-07-07T00:00:00Z",
|
||||
"note": "Dodano alias KO do PARTY_CANONICAL_MAPPINGS. 15 testow zielonych."
|
||||
},
|
||||
{
|
||||
"id": "T4",
|
||||
"title": "MarkdownRegistrySource test na fixture",
|
||||
"tag": null,
|
||||
"status": "done",
|
||||
"startedAt": "2026-07-07T00:00:00Z",
|
||||
"finishedAt": "2026-07-07T00:00:00Z",
|
||||
"note": "Fix isPersonTable: join all headers before check (Funkcja+Imię in different cells)."
|
||||
},
|
||||
{
|
||||
"id": "T5",
|
||||
"title": "Canonical write/read round-trip",
|
||||
"tag": null,
|
||||
"status": "done",
|
||||
"startedAt": "2026-07-07T00:00:00Z",
|
||||
"finishedAt": "2026-07-07T00:00:00Z",
|
||||
"note": "Round-trip write+read + assert hospital/person/affiliation/role data."
|
||||
},
|
||||
{
|
||||
"id": "T6",
|
||||
"title": "GraphLoader idempotencja",
|
||||
"tag": "NEO4J",
|
||||
"status": "skipped",
|
||||
"startedAt": "2026-07-07T00:00:00Z",
|
||||
"finishedAt": "2026-07-07T00:00:00Z",
|
||||
"note": "Docker API version mismatch: client 1.32, min 1.44. Testcontainers cannot start."
|
||||
},
|
||||
{
|
||||
"id": "T7",
|
||||
"title": "GraphValidator + overlap_report.csv",
|
||||
"tag": "NEO4J",
|
||||
"status": "skipped",
|
||||
"startedAt": null,
|
||||
"finishedAt": "2026-07-07T00:00:00Z",
|
||||
"note": "Docker API too old (same reason as T6)."
|
||||
},
|
||||
{
|
||||
"id": "T8",
|
||||
"title": "Zielony pelny build",
|
||||
"tag": null,
|
||||
"status": "done",
|
||||
"startedAt": null,
|
||||
"finishedAt": "2026-07-07T00:00:00Z",
|
||||
"note": "mvn verify green. 2 NEO4J skipped."
|
||||
},
|
||||
{
|
||||
"id": "T9",
|
||||
"title": "Person: title/pwzNumber",
|
||||
"tag": null,
|
||||
"status": "done",
|
||||
"startedAt": "2026-07-08T23:00:00Z",
|
||||
"finishedAt": "2026-07-08T23:03:00Z",
|
||||
"note": "Dodano title i pwzNumber do Person record. Zaktualizowano wszystkie nowicje Person (CanonicalWriter, Deduplicator, testy). 7 testow zielonych."
|
||||
},
|
||||
{
|
||||
"id": "T10",
|
||||
"title": "PwzRegistrySource",
|
||||
"tag": null,
|
||||
"status": "done",
|
||||
"startedAt": "2026-07-08T23:04:00Z",
|
||||
"finishedAt": "2026-07-08T23:11:00Z",
|
||||
"note": "PwzRegistrySource z PwzLookupClient. 4 testow zielonych: singleMatch, multipleMatches (ambiguity), noMatch, nullClient."
|
||||
},
|
||||
{
|
||||
"id": "T11",
|
||||
"title": "Organization/Company rekordy + round-trip",
|
||||
"tag": null,
|
||||
"status": "done",
|
||||
"startedAt": "2026-07-08T23:12:00Z",
|
||||
"finishedAt": "2026-07-08T23:23:00Z",
|
||||
"note": "Organization, Company, PersonOrganizationLink, OrganizationLink, PersonCompanyLink rekordy + LinkBasis enum. CanonicalDataset rozszerzony o 5 pól. Round-trip test zielony."
|
||||
},
|
||||
{
|
||||
"id": "T12",
|
||||
"title": "GraphLoader: Organization/Company",
|
||||
"tag": "NEO4J",
|
||||
"status": "skipped",
|
||||
"startedAt": "2026-07-08T23:25:00Z",
|
||||
"finishedAt": "2026-07-08T23:38:00Z",
|
||||
"note": "Docker API version mismatch: testcontainers docker-java client v1.32, min 1.44. Production code (GraphLoader GraphSchema) committed separately."
|
||||
},
|
||||
{
|
||||
"id": "T13",
|
||||
"title": "GraphValidator: konflikt interesow",
|
||||
"tag": "NEO4J",
|
||||
"status": "skipped",
|
||||
"startedAt": null,
|
||||
"finishedAt": "2026-07-08T23:38:00Z",
|
||||
"note": "Docker API too old (same reason as T12/T6/T7)."
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-295
@@ -1,295 +0,0 @@
|
||||
#!/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
@@ -1,189 +0,0 @@
|
||||
<# : 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"
|
||||
@@ -1,114 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,12 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
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<>(), null, null
|
||||
));
|
||||
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()), null, null
|
||||
));
|
||||
}
|
||||
}
|
||||
people.addAll(updatedPeople);
|
||||
}
|
||||
|
||||
CanonicalDataset dataset = new CanonicalDataset(
|
||||
"1.0", voivodeship, LocalDateTime.now(),
|
||||
hospitals, people, affiliations, roles, List.of(),
|
||||
List.of(), List.of(), List.of(), List.of(), 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;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
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),
|
||||
null, null
|
||||
);
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
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("[^\\p{L}\\s-]", "");
|
||||
normalized = transliteratePolish(normalized.toLowerCase());
|
||||
normalized = normalized.replaceAll("\\s+", "-").replaceAll("-+", "-");
|
||||
normalized = normalized.replaceAll("^-|-$", "");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String transliteratePolish(String input) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("ł", "l"); map.put("Ł", "l");
|
||||
map.put("ę", "e"); map.put("Ĕ", "e");
|
||||
map.put("ó", "o"); map.put("Ō", "o");
|
||||
map.put("ą", "a"); map.put("Ą", "a");
|
||||
map.put("ś", "s"); map.put("Ś", "s");
|
||||
map.put("ź", "z"); map.put("Ź", "z");
|
||||
map.put("ż", "z"); map.put("Ż", "z");
|
||||
map.put("ć", "c"); map.put("Ć", "c");
|
||||
map.put("ń", "n"); map.put("Ń", "n");
|
||||
String result = input;
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
result = result.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
static final List<String> TITLE_PREFIXES = List.of(
|
||||
"prof. dr hab.", "prof.dr hab.", "prof. dr.", "prof. dr",
|
||||
"dr hab.", "dr.hab.", "dr. hab.", "dr.hab",
|
||||
"mgr inż.", "mgr inż", "mgr.inż.", "mgr.inż",
|
||||
"prof.", "prof", "dr.", "dr",
|
||||
"mgr.", "mgr", "inż.", "inż",
|
||||
"lek.", "lek.med.", "n. med.", "n. o zdr.",
|
||||
"licencjat inż.", "licencjat inż"
|
||||
);
|
||||
|
||||
private String extractGivenAndFamilyNames(String rawName) {
|
||||
String name = rawName.trim();
|
||||
String lower = name.toLowerCase();
|
||||
for (String prefix : TITLE_PREFIXES) {
|
||||
if (lower.startsWith(prefix.toLowerCase())) {
|
||||
name = name.substring(prefix.length()).trim();
|
||||
lower = name.toLowerCase();
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
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("ko", "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;
|
||||
}
|
||||
}
|
||||
-367
@@ -1,367 +0,0 @@
|
||||
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()
|
||||
.map(Element::text)
|
||||
.collect(java.util.stream.Collectors.joining(" "))
|
||||
.contains("Funkcja") &&
|
||||
headerCells.stream()
|
||||
.map(Element::text)
|
||||
.collect(java.util.stream.Collectors.joining(" "))
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.developx.szpitale.ingest.source;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface PwzLookupClient {
|
||||
List<String> query(String fullName);
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package com.developx.szpitale.ingest.source;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class PwzRegistrySource {
|
||||
|
||||
private final PwzLookupClient client;
|
||||
|
||||
public PwzRegistrySource(PwzLookupClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public Optional<String> lookupPwz(String fullName) {
|
||||
if (client == null || fullName == null || fullName.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
List<String> results = client.query(fullName);
|
||||
|
||||
if (results == null || results.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (results.size() == 1) {
|
||||
return Optional.of(results.get(0));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
package com.developx.szpitale.ingest.source;
|
||||
|
||||
import com.developx.szpitale.model.RawHospitalRecord;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface VoivodeshipSource {
|
||||
String voivodeship();
|
||||
List<RawHospitalRecord> fetch();
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
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.File;
|
||||
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);
|
||||
}
|
||||
|
||||
public static CanonicalDataset readDataset(File file) throws IOException {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
return mapper.readValue(file, CanonicalDataset.class);
|
||||
}
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
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);
|
||||
loadOrganizations(session, dataset.organizations(), stats);
|
||||
loadCompanies(session, dataset.companies(), stats);
|
||||
loadPersonOrganizationLinks(session, dataset.personOrganizationLinks(), stats);
|
||||
loadOrganizationLinks(session, dataset.organizationLinks(), stats);
|
||||
loadPersonCompanyLinks(session, dataset.personCompanyLinks(), 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 void loadOrganizations(Session session, List<Organization> organizations, LoadStats stats) {
|
||||
if (organizations.isEmpty()) return;
|
||||
|
||||
List<Map<String, Object>> batch = new ArrayList<>();
|
||||
for (Organization o : organizations) {
|
||||
batch.add(Map.of(
|
||||
"id", o.id(),
|
||||
"name", o.name(),
|
||||
"krs", o.krs(),
|
||||
"nip", o.nip(),
|
||||
"sourceUrls", o.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 o " +
|
||||
"MERGE (org:Organization {id: o.id}) " +
|
||||
"SET org += {name: o.name, krs: o.krs, nip: o.nip, sourceUrls: o.sourceUrls}";
|
||||
session.run(cypher, Map.of("batch", subList));
|
||||
stats.addOrganizations(subList.size());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCompanies(Session session, List<Company> companies, LoadStats stats) {
|
||||
if (companies.isEmpty()) return;
|
||||
|
||||
List<Map<String, Object>> batch = new ArrayList<>();
|
||||
for (Company c : companies) {
|
||||
batch.add(Map.of(
|
||||
"id", c.id(),
|
||||
"name", c.name(),
|
||||
"krs", c.krs(),
|
||||
"nip", c.nip(),
|
||||
"ceidgId", c.ceidgId(),
|
||||
"sourceUrls", c.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 c " +
|
||||
"MERGE (comp:Company {id: c.id}) " +
|
||||
"SET comp += {name: c.name, krs: c.krs, nip: c.nip, ceidgId: c.ceidgId, sourceUrls: c.sourceUrls}";
|
||||
session.run(cypher, Map.of("batch", subList));
|
||||
stats.addCompanies(subList.size());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadPersonOrganizationLinks(Session session, List<PersonOrganizationLink> links, LoadStats stats) {
|
||||
if (links.isEmpty()) return;
|
||||
|
||||
List<Map<String, Object>> batch = new ArrayList<>();
|
||||
for (PersonOrganizationLink l : links) {
|
||||
batch.add(Map.of(
|
||||
"personId", l.personId(),
|
||||
"organizationId", l.organizationId(),
|
||||
"roleLabel", l.roleLabel(),
|
||||
"basis", l.basis().name(),
|
||||
"sourceUrls", l.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 l " +
|
||||
"MATCH (p:Person {id: l.personId}), (org:Organization {id: l.organizationId}) " +
|
||||
"MERGE (p)-[m:CZLONEK_ORGANIZACJI]->(org) " +
|
||||
"SET m += {roleLabel: l.roleLabel, basis: l.basis, sourceUrls: l.sourceUrls}";
|
||||
session.run(cypher, Map.of("batch", subList));
|
||||
stats.addPersonOrganizationLinks(subList.size());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadOrganizationLinks(Session session, List<OrganizationLink> links, LoadStats stats) {
|
||||
if (links.isEmpty()) return;
|
||||
|
||||
List<Map<String, Object>> batch = new ArrayList<>();
|
||||
for (OrganizationLink l : links) {
|
||||
batch.add(Map.of(
|
||||
"fromId", l.fromOrganizationId(),
|
||||
"toId", l.toOrganizationId(),
|
||||
"basis", l.basis().name(),
|
||||
"note", l.note(),
|
||||
"sourceUrls", l.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 l " +
|
||||
"MATCH (a:Organization {id: l.fromId}), (b:Organization {id: l.toId}) " +
|
||||
"MERGE (a)-[m:POWIAZANA_Z]->(b) " +
|
||||
"SET m += {basis: l.basis, note: l.note, sourceUrls: l.sourceUrls}";
|
||||
session.run(cypher, Map.of("batch", subList));
|
||||
stats.addOrganizationLinks(subList.size());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadPersonCompanyLinks(Session session, List<PersonCompanyLink> links, LoadStats stats) {
|
||||
if (links.isEmpty()) return;
|
||||
|
||||
List<Map<String, Object>> batch = new ArrayList<>();
|
||||
for (PersonCompanyLink l : links) {
|
||||
batch.add(Map.of(
|
||||
"personId", l.personId(),
|
||||
"companyId", l.companyId(),
|
||||
"roleLabel", l.roleLabel(),
|
||||
"basis", l.basis().name(),
|
||||
"sourceUrls", l.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 l " +
|
||||
"MATCH (p:Person {id: l.personId}), (comp:Company {id: l.companyId}) " +
|
||||
"MERGE (p)-[m:POWIAZANY_Z_FIRMA]->(comp) " +
|
||||
"SET m += {roleLabel: l.roleLabel, basis: l.basis, sourceUrls: l.sourceUrls}";
|
||||
session.run(cypher, Map.of("batch", subList));
|
||||
stats.addPersonCompanyLinks(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 int organizationCount;
|
||||
public int companyCount;
|
||||
public int personOrganizationLinkCount;
|
||||
public int organizationLinkCount;
|
||||
public int personCompanyLinkCount;
|
||||
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; }
|
||||
public void addOrganizations(int count) { organizationCount += count; }
|
||||
public void addCompanies(int count) { companyCount += count; }
|
||||
public void addPersonOrganizationLinks(int count) { personOrganizationLinkCount += count; }
|
||||
public void addOrganizationLinks(int count) { organizationLinkCount += count; }
|
||||
public void addPersonCompanyLinks(int count) { personCompanyLinkCount += count; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"Load stats - voivodeships: %s, hospitals: %d, people: %d, roles: %d, affiliations: %d, mandates: %d, organizations: %d, companies: %d, personOrgLinks: %d, orgLinks: %d, personCompanyLinks: %d",
|
||||
String.join(", ", voivodeships), hospitalCount, personCount, roleCount, affiliationCount, mandateCount,
|
||||
organizationCount, companyCount, personOrganizationLinkCount, organizationLinkCount, personCompanyLinkCount
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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 CONSTRAINT org_id IF NOT EXISTS FOR (o:Organization) REQUIRE o.id IS UNIQUE");
|
||||
session.run("CREATE CONSTRAINT company_id IF NOT EXISTS FOR (c:Company) REQUIRE c.id 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
) {
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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,
|
||||
@JsonProperty("organizations") List<Organization> organizations,
|
||||
@JsonProperty("companies") List<Company> companies,
|
||||
@JsonProperty("personOrganizationLinks") List<PersonOrganizationLink> personOrganizationLinks,
|
||||
@JsonProperty("organizationLinks") List<OrganizationLink> organizationLinks,
|
||||
@JsonProperty("personCompanyLinks") List<PersonCompanyLink> personCompanyLinks
|
||||
) {
|
||||
public CanonicalDataset(String schemaVersion, String voivodeship, LocalDateTime generatedAt,
|
||||
List<Hospital> hospitals, List<Person> people,
|
||||
List<Affiliation> affiliations, List<Role> roles,
|
||||
List<Mandate> mandates,
|
||||
List<Organization> organizations, List<Company> companies,
|
||||
List<PersonOrganizationLink> personOrganizationLinks,
|
||||
List<OrganizationLink> organizationLinks,
|
||||
List<PersonCompanyLink> personCompanyLinks) {
|
||||
this.schemaVersion = schemaVersion;
|
||||
this.voivodeship = voivodeship;
|
||||
this.generatedAt = generatedAt;
|
||||
this.hospitals = hospitals != null ? hospitals : List.of();
|
||||
this.people = people != null ? people : List.of();
|
||||
this.affiliations = affiliations != null ? affiliations : List.of();
|
||||
this.roles = roles != null ? roles : List.of();
|
||||
this.mandates = mandates != null ? mandates : List.of();
|
||||
this.organizations = organizations != null ? organizations : List.of();
|
||||
this.companies = companies != null ? companies : List.of();
|
||||
this.personOrganizationLinks = personOrganizationLinks != null ? personOrganizationLinks : List.of();
|
||||
this.organizationLinks = organizationLinks != null ? organizationLinks : List.of();
|
||||
this.personCompanyLinks = personCompanyLinks != null ? personCompanyLinks : List.of();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.developx.szpitale.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record Company(
|
||||
@JsonProperty("id") String id,
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("krs") String krs,
|
||||
@JsonProperty("nip") String nip,
|
||||
@JsonProperty("ceidgId") String ceidgId,
|
||||
@JsonProperty("sourceUrls") List<String> sourceUrls
|
||||
) {
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
) {
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
) {
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.developx.szpitale.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record Organization(
|
||||
@JsonProperty("id") String id,
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("krs") String krs,
|
||||
@JsonProperty("nip") String nip,
|
||||
@JsonProperty("sourceUrls") List<String> sourceUrls
|
||||
) {
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.developx.szpitale.model;
|
||||
|
||||
import com.developx.szpitale.model.enums.LinkBasis;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record OrganizationLink(
|
||||
@JsonProperty("fromOrganizationId") String fromOrganizationId,
|
||||
@JsonProperty("toOrganizationId") String toOrganizationId,
|
||||
@JsonProperty("basis") LinkBasis basis,
|
||||
@JsonProperty("note") String note,
|
||||
@JsonProperty("sourceUrls") List<String> sourceUrls
|
||||
) {
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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,
|
||||
@JsonProperty("title") String title,
|
||||
@JsonProperty("pwzNumber") String pwzNumber
|
||||
) {
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.developx.szpitale.model;
|
||||
|
||||
import com.developx.szpitale.model.enums.LinkBasis;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record PersonCompanyLink(
|
||||
@JsonProperty("personId") String personId,
|
||||
@JsonProperty("companyId") String companyId,
|
||||
@JsonProperty("roleLabel") String roleLabel,
|
||||
@JsonProperty("basis") LinkBasis basis,
|
||||
@JsonProperty("sourceUrls") List<String> sourceUrls
|
||||
) {
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.developx.szpitale.model;
|
||||
|
||||
import com.developx.szpitale.model.enums.LinkBasis;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record PersonOrganizationLink(
|
||||
@JsonProperty("personId") String personId,
|
||||
@JsonProperty("organizationId") String organizationId,
|
||||
@JsonProperty("roleLabel") String roleLabel,
|
||||
@JsonProperty("basis") LinkBasis basis,
|
||||
@JsonProperty("sourceUrls") List<String> sourceUrls
|
||||
) {
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
) {
|
||||
|
||||
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.developx.szpitale.model.enums;
|
||||
|
||||
public enum AffiliationType {
|
||||
PARTIA,
|
||||
KOMITET_WYBORCZY,
|
||||
KWW_LOKALNY
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.developx.szpitale.model.enums;
|
||||
|
||||
public enum ConfidenceLevel {
|
||||
POTWIERDZONA,
|
||||
NIEPOTWIERDZONA,
|
||||
NIEZWERYFIKOWANA,
|
||||
BRAK_DANYCH
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.developx.szpitale.model.enums;
|
||||
|
||||
public enum LegalForm {
|
||||
SPZOZ,
|
||||
SP_PSYCHIATRYCZNY_ZOZ,
|
||||
SPOLKA_Z_OO,
|
||||
INNY
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.developx.szpitale.model.enums;
|
||||
|
||||
public enum LinkBasis {
|
||||
KRS,
|
||||
NIP,
|
||||
CEIDG
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.developx.szpitale.model.enums;
|
||||
|
||||
public enum MandateType {
|
||||
RADNY_SEJMIKU,
|
||||
RADNY_POWIATU,
|
||||
RADNY_GMINY,
|
||||
WOJT_BURMISTRZ_PREZYDENT,
|
||||
STAROSTA,
|
||||
POSEL,
|
||||
SENATOR,
|
||||
MARSZALEK
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.developx.szpitale.model.enums;
|
||||
|
||||
public enum OrganType {
|
||||
DYREKCJA,
|
||||
RADA_SPOLECZNA,
|
||||
RADA_NADZORCZA
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.developx.szpitale.model.enums;
|
||||
|
||||
public enum RoleStatus {
|
||||
AKTUALNY,
|
||||
PELNIACY_OBOWIAZKI,
|
||||
ELEKT,
|
||||
BYLY
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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
@@ -1,6 +0,0 @@
|
||||
package com.developx.szpitale.model.enums;
|
||||
|
||||
public enum SupervisoryBodyType {
|
||||
RADA_SPOLECZNA,
|
||||
RADA_NADZORCZA
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
spring:
|
||||
neo4j:
|
||||
uri: bolt://localhost:7687
|
||||
authentication:
|
||||
username: neo4j
|
||||
password: password
|
||||
application:
|
||||
name: szpitale-graph
|
||||
|
||||
app:
|
||||
ingest:
|
||||
canonical-dir: canonical
|
||||
neo4j:
|
||||
batch-size: 1000
|
||||
-133
@@ -1,133 +0,0 @@
|
||||
package com.developx.szpitale.ingest;
|
||||
|
||||
import com.developx.szpitale.load.CanonicalReader;
|
||||
import com.developx.szpitale.model.*;
|
||||
import com.developx.szpitale.model.enums.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class OrganizationCompanyRoundTripTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void write_thenRead_preservesOrganizationsAndCompanies() throws Exception {
|
||||
Organization organization = new Organization(
|
||||
"org:fundacja-xyz",
|
||||
"Fundacja XYZ",
|
||||
"0000123456",
|
||||
null,
|
||||
List.of("https://ems.ms.gov.pl/fundacja-xyz")
|
||||
);
|
||||
|
||||
Company company = new Company(
|
||||
"company:abc-sp-zoo",
|
||||
"ABC sp. z o.o.",
|
||||
"0000654321",
|
||||
"1234567890",
|
||||
"CEIDG123",
|
||||
List.of("https://wyszukiwarka-krs.ms.gov.pl/abc")
|
||||
);
|
||||
|
||||
Person person = new Person(
|
||||
"person:jan-kochanowicz",
|
||||
"Jan Kochanowicz",
|
||||
"prof. dr hab. Jan Kochanowicz",
|
||||
List.of("prof.", "dr hab."),
|
||||
List.of("https://uskwb.pl/"),
|
||||
"prof. dr hab.",
|
||||
null
|
||||
);
|
||||
|
||||
PersonOrganizationLink pol = new PersonOrganizationLink(
|
||||
"person:jan-kochanowicz",
|
||||
"org:fundacja-xyz",
|
||||
"Prezes Zarządu",
|
||||
LinkBasis.KRS,
|
||||
List.of("https://ems.ms.gov.pl/fundacja-xyz")
|
||||
);
|
||||
|
||||
PersonCompanyLink pcl = new PersonCompanyLink(
|
||||
"person:jan-kochanowicz",
|
||||
"company:abc-sp-zoo",
|
||||
"Wspólnik",
|
||||
LinkBasis.KRS,
|
||||
List.of("https://wyszukiwarka-krs.ms.gov.pl/abc")
|
||||
);
|
||||
|
||||
OrganizationLink orgLink = new OrganizationLink(
|
||||
"org:fundacja-xyz",
|
||||
"org:inna-fundacja",
|
||||
LinkBasis.NIP,
|
||||
"wspolny adres",
|
||||
List.of("https://ems.ms.gov.pl/fundacja-xyz")
|
||||
);
|
||||
|
||||
CanonicalDataset dataset = new CanonicalDataset(
|
||||
"1.0",
|
||||
"podlaskie",
|
||||
LocalDateTime.now(),
|
||||
List.of(),
|
||||
List.of(person),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(organization),
|
||||
List.of(company),
|
||||
List.of(pol),
|
||||
List.of(orgLink),
|
||||
List.of(pcl)
|
||||
);
|
||||
|
||||
Path outputFile = tempDir.resolve("test-canonical.json");
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
mapper.writeValue(outputFile.toFile(), dataset);
|
||||
|
||||
CanonicalDataset readResult = CanonicalReader.readDataset(outputFile.toFile());
|
||||
|
||||
assertNotNull(readResult.organizations());
|
||||
assertNotNull(readResult.companies());
|
||||
assertNotNull(readResult.personOrganizationLinks());
|
||||
assertNotNull(readResult.organizationLinks());
|
||||
assertNotNull(readResult.personCompanyLinks());
|
||||
|
||||
assertEquals(1, readResult.organizations().size());
|
||||
assertEquals("org:fundacja-xyz", readResult.organizations().get(0).id());
|
||||
assertEquals("Fundacja XYZ", readResult.organizations().get(0).name());
|
||||
assertEquals("0000123456", readResult.organizations().get(0).krs());
|
||||
|
||||
assertEquals(1, readResult.companies().size());
|
||||
assertEquals("company:abc-sp-zoo", readResult.companies().get(0).id());
|
||||
assertEquals("ABC sp. z o.o.", readResult.companies().get(0).name());
|
||||
assertEquals("1234567890", readResult.companies().get(0).nip());
|
||||
assertEquals("CEIDG123", readResult.companies().get(0).ceidgId());
|
||||
|
||||
assertEquals(1, readResult.personOrganizationLinks().size());
|
||||
assertEquals("person:jan-kochanowicz", readResult.personOrganizationLinks().get(0).personId());
|
||||
assertEquals("org:fundacja-xyz", readResult.personOrganizationLinks().get(0).organizationId());
|
||||
assertEquals(LinkBasis.KRS, readResult.personOrganizationLinks().get(0).basis());
|
||||
|
||||
assertEquals(1, readResult.organizationLinks().size());
|
||||
assertEquals("org:fundacja-xyz", readResult.organizationLinks().get(0).fromOrganizationId());
|
||||
assertEquals("org:inna-fundacja", readResult.organizationLinks().get(0).toOrganizationId());
|
||||
assertEquals(LinkBasis.NIP, readResult.organizationLinks().get(0).basis());
|
||||
|
||||
assertEquals(1, readResult.personCompanyLinks().size());
|
||||
assertEquals("person:jan-kochanowicz", readResult.personCompanyLinks().get(0).personId());
|
||||
assertEquals("company:abc-sp-zoo", readResult.personCompanyLinks().get(0).companyId());
|
||||
assertEquals(LinkBasis.KRS, readResult.personCompanyLinks().get(0).basis());
|
||||
}
|
||||
}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
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"),
|
||||
null, null
|
||||
);
|
||||
Person janKowaski = new Person(
|
||||
"person:jan-kowaski",
|
||||
"Jan Kowaski",
|
||||
"Jan Kowaski",
|
||||
List.of(),
|
||||
List.of("https://example.com/2"),
|
||||
null, null
|
||||
);
|
||||
|
||||
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"),
|
||||
null, null
|
||||
);
|
||||
Person second = new Person(
|
||||
"person:jan-kowalski",
|
||||
"Jan Kowalski",
|
||||
"Jan Kowalski",
|
||||
List.of(),
|
||||
List.of("https://example.com/2"),
|
||||
null, null
|
||||
);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
-132
@@ -1,132 +0,0 @@
|
||||
package com.developx.szpitale.ingest.normalize;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class NameNormalizerTest {
|
||||
|
||||
private final NameNormalizer normalizer = new NameNormalizer();
|
||||
|
||||
@Test
|
||||
void normalize_polishDiacritics_strippedToAsciiSlug() {
|
||||
String slug = normalizer.makeSlug("Łukasz Żółć");
|
||||
assertEquals("lukasz-zolc", slug);
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalize_slug_lowercaseSeparator() {
|
||||
String slug = normalizer.makeSlug("Jan Kowalski");
|
||||
assertEquals("jan-kowalski", slug);
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalize_slug_multipleSpacesToOne() {
|
||||
String slug = normalizer.makeSlug("Jan Kowalski");
|
||||
assertEquals("jan-kowalski", slug);
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalize_slug_removesSpecialChars() {
|
||||
String slug = normalizer.makeSlug("Dr. Jan Kowalski Jr.");
|
||||
assertEquals("dr-jan-kowalski-jr", slug);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"'prof. dr hab. Jan Kochanowicz', 'Jan Kochanowicz'",
|
||||
"'dr Jan Nowak', 'Jan Nowak'",
|
||||
"'mgr inż. Anna Kowalska', 'Anna Kowalska'",
|
||||
"'Jan Wiśniewski', 'Jan Wiśniewski'",
|
||||
"'prof. Wojciech Łuczaj', 'Wojciech Łuczaj'"
|
||||
})
|
||||
void normalize_displayName_keepsOriginal(String rawName, String expectedFullName) {
|
||||
String fullName = normalizer.extractFullName(rawName);
|
||||
assertEquals(expectedFullName, fullName);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"'prof. dr hab. Jan Kochanowicz', 'prof.'",
|
||||
"'prof. dr hab. Jan Kochanowicz', 'dr hab.'",
|
||||
"'dr hab. n. med. Jan Wiśniewski', 'dr hab.'",
|
||||
"'dr hab. n. med. Jan Wiśniewski', 'n. med.'",
|
||||
"'mgr inż. Anna Kowalska', 'mgr inż.'"
|
||||
})
|
||||
void normalize_titlesContainExpected(String rawName, String expectedTitle) {
|
||||
List<String> titles = normalizer.extractTitles(rawName);
|
||||
assertTrue(titles.contains(expectedTitle), "Expected title '" + expectedTitle + "' not found in " + titles);
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalize_titlesEmptyForPlainName() {
|
||||
List<String> titles = normalizer.extractTitles("Jan Nowak");
|
||||
assertTrue(titles.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalize_emptyTitlesWhenNoTitles() {
|
||||
List<String> titles = normalizer.extractTitles("Jan Kowalski");
|
||||
assertTrue(titles.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void makePersonId_createsPersonSlug() {
|
||||
String id = normalizer.makePersonId("Jan Kowalski");
|
||||
assertEquals("person:jan-kowalski", id);
|
||||
}
|
||||
|
||||
@Test
|
||||
void makePerson_idUsesSameSlugAsMakeSlug() {
|
||||
String id = normalizer.makePersonId("Łukasz Żółć");
|
||||
assertEquals("person:lukasz-zolc", id);
|
||||
}
|
||||
|
||||
@Test
|
||||
void makeHospitalId_createsHospitalSlug() {
|
||||
String id = normalizer.makeHospitalId("podlaskie", "Uniwersytecki Szpital Kliniczny");
|
||||
assertEquals("hosp:podlaskie:uniwersytecki-szpital-kliniczny", id);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fuzzySimilarity_identicalStringsReturn1() {
|
||||
double similarity = normalizer.fuzzySimilarity("Jan Kowalski", "Jan Kowalski");
|
||||
assertEquals(1.0, similarity, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fuzzySimilarity_typoReturnsHigh() {
|
||||
double similarity = normalizer.fuzzySimilarity("Jan Kowalski", "Jan Kowaski");
|
||||
assertTrue(similarity > 0.9, "Typo 'Kowalski' vs 'Kowaski' should have very high similarity: " + similarity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fuzzySimilarity_differentNamesReturnLower() {
|
||||
double similarity = normalizer.fuzzySimilarity("Jan Kowalski", "Anna Nowak");
|
||||
assertTrue(similarity < 0.7, "Two completely different names should have moderate similarity: " + similarity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fuzzySimilarity_handlesPolishCharacters() {
|
||||
double similarity = normalizer.fuzzySimilarity("Jan Kowalski", "Jan Kowalscy");
|
||||
assertTrue(similarity > 0.8, "Names differing only by suffix should have high similarity: " + similarity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findDuplicateCandidates_returnsCandidatesAboveThreshold() {
|
||||
List<String> names = List.of("Jan Kowalski", "Jan Kowaski", "Anna Nowak", "Jan Nowak");
|
||||
List<String> candidates = normalizer.findDuplicateCandidates(names, "Jan Kowalski", 0.7);
|
||||
assertTrue(candidates.contains("Jan Kowaski"), "Should contain similar name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findDuplicateCandidates_filtersTargetName() {
|
||||
List<String> names = List.of("Jan Kowalski");
|
||||
List<String> candidates = normalizer.findDuplicateCandidates(names, "Jan Kowalski", 0.7);
|
||||
assertTrue(candidates.isEmpty(), "Should not return the target name itself");
|
||||
}
|
||||
}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
package com.developx.szpitale.ingest.normalize;
|
||||
|
||||
import com.developx.szpitale.model.enums.AffiliationType;
|
||||
import com.developx.szpitale.model.enums.ConfidenceLevel;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class PartyNormalizerTest {
|
||||
|
||||
private final PartyNormalizer normalizer = new PartyNormalizer();
|
||||
|
||||
@Test
|
||||
void canonicalize_poAlias_toKoalicjaObywatelska() {
|
||||
assertEquals("Koalicja Obywatelska", normalizer.canonicalize("PO"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalize_koAlias_toKoalicjaObywatelska() {
|
||||
assertEquals("Koalicja Obywatelska", normalizer.canonicalize("KO"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalize_pisAlias_toPrawoISprawiedliwosc() {
|
||||
assertEquals("Prawo i Sprawiedliwość", normalizer.canonicalize("PiS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalize_fullName_psl() {
|
||||
assertEquals("PSL", normalizer.canonicalize("PSL"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalize_fullName_polska2050() {
|
||||
assertEquals("Polska 2050", normalizer.canonicalize("Polska 2050"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalize_undefined_name_returnsOriginal() {
|
||||
String result = normalizer.canonicalize("Nowa Partia");
|
||||
assertEquals("Nowa Partia", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalize_containsAlias_matchesSubstring() {
|
||||
String result = normalizer.canonicalize("KWW Nowak");
|
||||
assertEquals("KWW_LOKALNY", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void determineType_kww_returnsKWW_LOKALNY() {
|
||||
assertEquals(AffiliationType.KWW_LOKALNY, normalizer.determineType("KWW Nowak"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void determineType_komitet_wyborczy_returnsKOMITET() {
|
||||
assertEquals(AffiliationType.KOMITET_WYBORCZY, normalizer.determineType("Komitet Wyborczy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void determineType_unknown_returnsPARTIA() {
|
||||
assertEquals(AffiliationType.PARTIA, normalizer.determineType("Prawo i Sprawiedliwość"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapConfidence_niezweryfikowane_toNIEZWERYFIKOWANA() {
|
||||
assertEquals(ConfidenceLevel.NIEZWERYFIKOWANA, normalizer.mapConfidence("NIEZWERYFIKOWANE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapConfidence_brak_danych_toBRAK_DANYCH() {
|
||||
assertEquals(ConfidenceLevel.BRAK_DANYCH, normalizer.mapConfidence("brak danych"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapConfidence_niepotwierdzona_toNIEPOTWIERDZONA() {
|
||||
assertEquals(ConfidenceLevel.NIEPOTWIERDZONA, normalizer.mapConfidence("brak/niepotwierdzona"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapConfidence_null_toBRAK_DANYCH() {
|
||||
assertEquals(ConfidenceLevel.BRAK_DANYCH, normalizer.mapConfidence(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapConfidence_confirmed_default() {
|
||||
assertEquals(ConfidenceLevel.POTWIERDZONA, normalizer.mapConfidence("potwierdzone"));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user