Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,20 @@ fi

# --- all five suites (0-4): only when Python changed ------------------------
# run_tests.py repeats suite 0 and adds the static payroll and the three generated
# suites. tools/ is included too - tools/preflight.py and tools/k_checker.py are Python
# just as much as anything under test/, and a change there was previously invisible
# here and caught only in CI. There is no personal-data step here: that guard runs in
# CI, over the text files and inside every tracked spreadsheet.
if git diff --cached --name-only | grep -qE '^(test|tools)/.*\.py$'; then
# suites. skills/trz-expert/scripts/ is included too - preflight.py and k_checker.py
# are Python just as much as anything under test/, and a change there was previously
# invisible here and caught only in CI. There is no personal-data step here: that guard
# runs in CI, over the text files and inside every tracked spreadsheet.
if git diff --cached --name-only | grep -qE '^(test/.*\.py|skills/trz-expert/scripts/.*\.py)$'; then
if ! "$PY" -c 'import openpyxl' 2>/dev/null; then
echo
echo "pre-commit: Python under test/ or tools/ was touched but openpyxl is"
echo " missing - the five suites did NOT run. Create the environment once:"
echo "pre-commit: Python under test/ or skills/trz-expert/scripts/ was touched but"
echo " openpyxl is missing - the five suites did NOT run. Create the environment once:"
echo " python3 -m venv .venv && .venv/bin/pip install -r test/requirements.txt"
echo "Commit stopped so that unchecked code does not land. Skip: --no-verify"
exit 1
fi
echo "pre-commit: Python under test/ or tools/ was touched - running all five suites (0-4), 25 seeds"
echo "pre-commit: Python under test/ or skills/trz-expert/scripts/ was touched - running all five suites (0-4), 25 seeds"
if ! "$PY" test/run_tests.py --seeds 25 > /tmp/trz-suites.$$ 2>&1; then
tail -40 /tmp/trz-suites.$$
rm -f /tmp/trz-suites.$$
Expand All @@ -100,7 +100,7 @@ if git diff --cached --name-only | grep -qE '^(test|tools)/.*\.py$'; then
cat /tmp/trz-kchecker.$$
rm -f /tmp/trz-kchecker.$$
echo
echo "Commit stopped: tools/k_checker.py disagrees with generate_wide.py's"
echo "Commit stopped: k_checker.py disagrees with generate_wide.py's"
echo "manifest. To skip: git commit --no-verify"
exit 1
fi
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,16 @@ jobs:
run: python test/skill_test.py

- name: Pre-flight check for real payroll workbooks
# tools/preflight.py is what a real export meets before the audit. Its
# scripts/preflight.py is what a real export meets before the audit. Its
# vocabulary is pinned against trz_model.COLUMNS here, so renaming a canonical
# column cannot quietly send real ones to the unknown list, and its fixtures
# are built in memory - no payroll, real or generated, is read.
run: python test/preflight_test.py

- name: K5/K6 computed from a workbook, against generate_wide.py's manifest
# tools/k_checker.py's own hand-built fixture, plus 300 seeds of the same
# scripts/k_checker.py's own hand-built fixture, plus 300 seeds of the same
# generated payrolls suite 2 checks - the depth this job runs everything else
# at. 2.14.0 shipped this checker walking only tools/preflight.py's known-concept
# at. 2.14.0 shipped this checker walking only scripts/preflight.py's known-concept
# columns and went 0 for 28 on K5 against these same fixtures; this step is
# what would have caught it.
run: python test/k_checker_test.py --seeds 300
Expand Down
45 changes: 25 additions & 20 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ python test/rates_test.py # rates vs. the reference file. No dependencies. R
python test/skill_test.py # packaging: frontmatter, references, manifests, licences, dates
python test/checks_test.py # suite 1: static payroll against the key in expected_findings.md
python test/eval_skill.py --selftest # free: checks the refusal grading itself
python test/preflight_test.py # tools/preflight.py: clean is silent, each shape defect found once
python test/k_checker_test.py # tools/k_checker.py: K5/K6 vs. generate_wide.py's manifest, 60 seeds by default
python test/preflight_test.py # scripts/preflight.py: clean is silent, each shape defect found once
python test/k_checker_test.py # scripts/k_checker.py: K5/K6 vs. generate_wide.py's manifest, 60 seeds by default
python test/komplekt_test.py # suite 5: ведомост -> обр. 1 -> обр. 6 -> внесено -> счетоводство, one link at a time
python test/lifecycle_test.py # suite 6: five months of the same people, one timeline break at a time
python test/run_tests.py # all five, 50 seeds
Expand Down Expand Up @@ -84,7 +84,7 @@ A false positive fails exactly like a miss.
- **`stavki.md` is an index too, since 2.14.4** — statuses, the per-section
verification-date table (now with a file column) and the changelog, plus a one-line
"Ставки по теми" bullet per topic. The rate tables themselves are in
`references/stavki/<topic>.md`. `test/rates_test.py`'s `TEXT`, `tools/preflight.py`'s
`references/stavki/<topic>.md`. `test/rates_test.py`'s `TEXT`, `skills/trz-expert/scripts/preflight.py`'s
`regime_boundaries()` and `test/findings.py`'s citation grounding all read the index
plus every topic file concatenated, not the index alone - a rate or a citation moving
into a topic file must not go blind to any of the three. `skill_test.py` pins the
Expand All @@ -108,21 +108,26 @@ A false positive fails exactly like a miss.
`references/proverki/` too, not only the index, since a citation can now live in either
one. Edit the group file's content; keep the index's title line in step only if the
title itself changed.
- **`tools/` is not part of the skill.** `tools/preflight.py` checks whether a real
payroll workbook can be audited at all — header row, formulas, period, missing
columns, and the two values no file carries (КИД and ТЗПБ). It lives outside
`skills/trz-expert` on purpose: SKILL.md promises prose only, and installing copies
the skill directory whole. It never writes to the workbook — the file is evidence —
and never guesses a period, because guessing the period picks the thresholds. Its
column vocabulary is pinned against `trz_model.COLUMNS` by `preflight_test.py`.
`tools/k_checker.py` sits beside it, same reasoning, and computes only K5 (a
hand-typed total) and K6 (rounding) from a real workbook — the two group-K checks
that ask nothing about any column but the one being checked. It walks every header on
the sheet, not only `preflight.py`'s known concepts: limiting it to those once meant
0 of 28 injected K5 defects were found, because a real file's benefit and deduction
columns are not all in that closed vocabulary. `test/k_checker_test.py` checks it
against `generate_wide.py`'s manifest, not only a hand-built fixture, for exactly
that reason.
- **`skills/trz-expert/scripts/` ships with the plugin, since 2.16.0.** It used to be
`tools/` at the repo root, outside `skills/trz-expert`, because installing a plugin
copies the skill directory whole and `SKILL.md` once promised prose only. That promise
cost every installed user the two scripts entirely — they could only ever be reached
from a cloned checkout, never from `/plugin install`. `scripts/preflight.py` checks
whether a real payroll workbook can be audited at all — header row, formulas, period,
missing columns, and the two values no file carries (КИД and ТЗПБ). It never writes to
the workbook — the file is evidence — and never guesses a period, because guessing the
period picks the thresholds. Its column vocabulary is pinned against `trz_model.COLUMNS`
by `preflight_test.py`. `scripts/k_checker.py` sits beside it, same reasoning, and
computes only K5 (a hand-typed total) and K6 (rounding) from a real workbook — the two
group-K checks that ask nothing about any column but the one being checked. It walks
every header on the sheet, not only `preflight.py`'s known concepts: limiting it to
those once meant 0 of 28 injected K5 defects were found, because a real file's benefit
and deduction columns are not all in that closed vocabulary. `test/k_checker_test.py`
checks it against `generate_wide.py`'s manifest, not only a hand-built fixture, for
exactly that reason. Moving the directory took `sys.path` edits in both test files,
`preflight.py`'s own `SKILL_DIR`-relative path to `stavki.md`, and the pre-commit
hook's trigger regex - a change under `scripts/` that stops matching that regex would
go untested locally again, the exact failure mode the move was meant to close.
- **Suite 6 may only compare a month with another month.** Every sheet in
`test/generate_lifecycle.py` is internally correct on purpose — the arithmetic
reconciles, the bases are right, each month would pass suites 1–4 alone. The only thing
Expand All @@ -144,8 +149,8 @@ A false positive fails exactly like a miss.
its own signal and nothing else — a false positive fails like a miss, as everywhere
else here. Adding a shape means a mutation in `generate_shapes.py`, its signal in
`SHAPES`, and proving it: break the detection, watch it go red, revert.
- **A company's layout is declared once** in a `mapping.yaml`
(`tools/mapping.example.yaml`), not re-guessed monthly. A typo in a concept key
- **A company's layout is declared once** in a `mapping.yaml`, templated by
`scripts/mapping.example.yaml`, not re-guessed monthly. A typo in a concept key
blocks rather than doing nothing quietly, and a mapping pointing at a column that is
no longer there is reported as stale. The file holds headers and КИД only — no
personal data — so it belongs in version control.
Expand Down
23 changes: 12 additions & 11 deletions skills/trz-expert/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
name: trz-expert
description: Старши експертиза по ТРЗ (труд и работна заплата) за България. Анализира ведомости, фишове за заплати, трудови договори, графици и присъствени форми спрямо Кодекса на труда, КСО, ЗДДФЛ и Наредбата за структурата и организацията на работната заплата. Използвай при работа с ведомост, рекапитулация, фиш за заплата, трудов договор, допълнително споразумение, график при СИРВ, осигуровки, МОД, МРЗ, извънреден труд, нощен труд, клас прослужено време, обезщетение при уволнение, удръжки и запори върху заплата, или когато потребителят иска проверка дали заплащането в дадена фирма е законосъобразно. Also use for English requests to audit or check a Bulgarian payroll, payslip, employment contract or shift schedule for compliance with Bulgarian labour, social-security and income-tax law.
license: CC-BY-4.0
compatibility: Prose and reference material only; no bundled scripts. Needs a Bulgarian
payroll context and a working Python with openpyxl for spreadsheet work. The rates in
references/stavki.md were verified on 01.09.2026 and must be re-verified for any later
period; the skill refuses to guess one. Installed as a Claude Code plugin it asks one question when
enabled; anywhere else that answer is absent and the documented default applies.
compatibility: Bundles scripts/preflight.py and scripts/k_checker.py, both read-only
against the workbook. Needs a Bulgarian payroll context and a working Python with
openpyxl for spreadsheet work. The rates in references/stavki.md were verified on
01.09.2026 and must be re-verified for any later period; the skill refuses to guess
one. Installed as a Claude Code plugin it asks one question when enabled; anywhere
else that answer is absent and the documented default applies.
metadata:
jurisdiction: BG
rates_verified: '2026-09-01'
Expand Down Expand Up @@ -145,9 +146,9 @@ fs = openpyxl.load_workbook(path, data_only=False) # формулите
ръчни сборове, съседни листове — и какво да правиш, когато формулите не са достъпни, е в
`references/proverki/k.md`, раздел „Как се чете електронна таблица“ над самата група K.

За K5 (ръчно вписан сбор) и K6 (закръгляване) конкретно репото носи `tools/k_checker.py` —
смята и двете директно от файла, без допускания за смисъла на никоя друга колона. Когато
можеш да го изпълниш, пусни го и вземи находките му вместо да ги пресмяташ наум; другите
За K5 (ръчно вписан сбор) и K6 (закръгляване) конкретно скилът носи
`scripts/k_checker.py` — смята и двете директно от файла, без допускания за смисъла на
никоя друга колона. Пусни го и вземи находките му вместо да ги пресмяташ наум; другите
шест проверки на групата остават на анализа тук.

## Работен процес
Expand Down Expand Up @@ -221,9 +222,9 @@ fs = openpyxl.load_workbook(path, data_only=False) # формулите
той знае файла си.

Липсваща колона е находка сама по себе си — отбележи я, не я запълвай с допускане.
`tools/preflight.py` в репозиторито на скила прави точно това съответствие с
речник от понятия и `mapping.yaml` на дружеството; когато докладът му е наличен, вземи
съответствията оттам, вместо да ги установяваш наново.
`scripts/preflight.py` прави точно това съответствие с речник от понятия и
`scripts/mapping.example.yaml` на дружеството; пусни го и вземи съответствията оттам,
вместо да ги установяваш наново.

### 3а. Какво изобщо може да се провери с подаденото

Expand Down
4 changes: 2 additions & 2 deletions skills/trz-expert/references/proverki.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
- **неприложима** — материята я няма в този случай: няма дежурство, няма запор, няма
прекратяване през периода. Назови причината, но не искай документ — такъв не се дължи.

`tools/preflight.py` в това репозитори именно това съобщава, преди одитът да тръгне:
`scripts/preflight.py` именно това съобщава, преди одитът да тръгне:
`NO_FORMULAS`, `MERGED_IN_DATA`, `NO_CACHED_VALUES`, `ERROR_CELLS` и `NUMBERS_AS_TEXT`
са „непроверимо“; липсващият КИД е „недостатъчни данни“. Когато докладът му е наличен, вземи състоянията
са „непроверимо“; липсващият КИД е „недостатъчни данни“. Пусни го и вземи състоянията
оттам, вместо да ги установяваш наново.

**`EXTERNAL_LINKS` също е „непроверимо“ за колоните, които зависят от връзката:** числото
Expand Down
4 changes: 2 additions & 2 deletions skills/trz-expert/references/proverki/k.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ fs = openpyxl.load_workbook(path, data_only=False) # формулите
който намери друг дефект в същата колона два реда по-долу. Гледана е била колоната, не
стойностите.

**`tools/k_checker.py` в репозиторито на скила смята K5 и K6 директно от файла** —
**`scripts/k_checker.py` смята K5 и K6 директно от файла** —
сборът срещу сумата на клетките, закръгляването срещу самото число — вместо наум по
таблицата. Обхватът му е нарочно само тези две: другите шест на групата остават на
анализа тук. Когато докладът му е наличен, вземи находките оттам, вместо да ги
анализа тук. Пусни го и вземи находките оттам, вместо да ги
пресмяташ отново.
- **K7. Разход за труд** · *Cost of labour.*
Общият разход трябва да е брутото плюс вноските за сметка на работодателя плюс
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
they compare the file with itself. That should make all eight easy to hand to code, but
most of them need something a generic tool cannot promise: K1 needs the full set of
accrual columns, and a real file legitimately carries accrual columns this tool's closed
concept vocabulary (`tools/preflight.py`'s CONCEPTS) does not name - a bonus, a
concept vocabulary (`skills/trz-expert/scripts/preflight.py`'s CONCEPTS) does not name - a bonus, a
severance payment, a benefit. Flagging БРУТО for not equalling the columns this tool
happens to recognise would be a false positive on every clean file that has one more
accrual column than the vocabulary does, and this project treats a false positive as no
Expand All @@ -22,7 +22,7 @@
column is not money, because its totals row is often an average rather than a sum, and
"totals row" alone cannot tell the two apart.

Reuses tools/preflight.py's column resolution (Mapping, classify()) so a company's
Reuses skills/trz-expert/scripts/preflight.py's column resolution (Mapping, classify()) so a company's
layout is declared once, in one file, not twice - but neither check is limited to the
concept vocabulary. 2.14.0 iterated `analyse()`'s known-concept columns only, and missed
every K5 defect landing in a column outside that closed list (a benefit column, a
Expand Down Expand Up @@ -54,7 +54,7 @@
up (a rate column rarely has a sum worth checking anyway) to remove the false positive.

Usage:
python tools/k_checker.py ВЕДОМОСТ.xlsx [--mapping tools/mapping.example.yaml]
python skills/trz-expert/scripts/k_checker.py ВЕДОМОСТ.xlsx [--mapping skills/trz-expert/scripts/mapping.example.yaml]
[--kid 62] [--group 3] [--tzpb 0.4] [--out report.md]

Exit codes: 0 nothing found, 1 at least one finding, 2 could not read the file.
Expand Down Expand Up @@ -104,7 +104,7 @@ def _is_money_like(header, concept):
def check(path, mapping=None, kid=None, group=None, tzpb=None):
"""Findings as a list of dicts. Never writes; reads the workbook once, values only.

Every header on the sheet is walked, not only the ones tools/preflight.py's closed
Every header on the sheet is walked, not only the ones skills/trz-expert/scripts/preflight.py's closed
concept vocabulary recognises - K5 and K6 are arithmetic on one column at a time and
do not need to know what the column means. K6 finds at most one column per row
(headers in sheet order, first money-like column that fails); K5 checks every
Expand Down
Loading