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
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ jobs:
# Rejects raw error_code="LITERAL" string literals (must use ErrorCode).
run: uv run python scripts/check_error_codes.py

- name: File-size budget check
# Enforces the CONTRIBUTING.md per-layer budgets in CODE LINES
# (docstrings/comments excluded, so documenting a module is free).
# Files already over their hard ceiling are grandfathered in
# scripts/file_size_baseline.json and may only shrink -- this catches
# NEW oversized modules and growth of the existing ones, which review
# reliably misses because a diff never shows the resulting file size.
run: uv run python scripts/check_file_size.py

# ────────────────────────────────────────────────────────────────────────
# Test suite across every supported interpreter. pyproject declares
# `requires-python = ">=3.12"`, so the matrix is 3.12 + 3.13 (3.10/3.11 are
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Full author checklist: see `CONTRIBUTING.md` > "Releasing a beta (pre-release) v

## Coding Conventions

> **0. (BINDING) Follow [CONTRIBUTING.md](CONTRIBUTING.md) in full.** Every code change -- human or AI agent -- must satisfy the rules in `CONTRIBUTING.md`. Specifically, the "Code Quality Patterns" section is non-negotiable: dataclasses (not bare tuples) for multi-value returns; categorical arguments before variable ones; `ErrorCode` enum (never raw strings); file-size budgets; context managers over lambdas; named functions over assigned anonymous functions; `ty` clean for new code. The `.claude/settings.json` post-edit hooks run `ruff check --fix`, `ruff format`, and `ty check` after every edit -- when an AI agent edits a file in this repo, those checks fire automatically and any failure must be addressed before continuing. If a rule conflicts with an existing pattern in legacy code, **fix it in the PR you are touching** or open a follow-up issue; do not propagate the pattern.
> **0. (BINDING) Follow [CONTRIBUTING.md](CONTRIBUTING.md) in full.** Every code change -- human or AI agent -- must satisfy the rules in `CONTRIBUTING.md`. Specifically, the "Code Quality Patterns" section is non-negotiable: dataclasses (not bare tuples) for multi-value returns; categorical arguments before variable ones; `ErrorCode` enum (never raw strings); file-size budgets (measured in CODE LINES -- docstrings and comments are free; `make loc-check`); context managers over lambdas; named functions over assigned anonymous functions; `ty` clean for new code. The `.claude/settings.json` post-edit hooks run `ruff check --fix`, `ruff format`, and `ty check` after every edit -- when an AI agent edits a file in this repo, those checks fire automatically and any failure must be addressed before continuing. If a rule conflicts with an existing pattern in legacy code, **fix it in the PR you are touching** or open a follow-up issue; do not propagate the pattern.

1. **Typer commands** are thin - they parse arguments, call a service, and format output. No business logic in commands.

Expand Down
27 changes: 22 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,32 @@ If a new category appears, **add it to `ErrorCode`** and `_ERROR_CODE_TO_TYPE` i

### File-size budgets -- split when concerns drift

Hard ceiling per file:
Budgets are measured in **code lines**, not raw line count. Docstrings, comments and blank lines do **not** count.

```bash
make loc-check # the gate; part of `make check`
make loc-report # every module by code lines, largest first
make loc-baseline # re-record grandfathered files AFTER a split
```

| Layer | Soft ceiling | Hard ceiling |
|-------|--------------|--------------|
| `commands/*.py` | 800 LOC | 1200 LOC |
| `services/*.py` | 1000 LOC | 1500 LOC |
| `client/*.py` (per module) / `manage_client.py` | 1500 LOC | 2000 LOC |
| `commands/*.py` | 800 | 1200 |
| `services/*.py` | 1000 | 1500 |
| `client/*.py` (per module) / `manage_client.py` | 1500 | 2000 |
| `server/*.py` | 800 | 1200 |
| `sync/*.py` | 1000 | 1500 |
| everything else in the package | 1000 | 1500 |

**Why code lines and not LOC.** This codebase deliberately writes long rationale-carrying docstrings -- they are the reason it stays navigable, for humans and for the AI agents that work in it. A raw-LOC budget taxes exactly that and pushes toward *less* explanation, which is backwards. The gap is not marginal: `services/version_service.py` is 1252 lines but 705 lines of code (36% prose), and `constants.py` is 574 lines but 190 lines of code (56% prose). Run `make loc-report` for the current numbers rather than trusting these.

The line the metric draws: a **docstring** (the bare leading string of a module, class or function) is prose and is exempt. A string **assigned to a name** -- a SQL block, a template, the `CHANGELOG` tables -- is data, is counted, and cannot be used to hide content from the budget.

**Soft vs hard.** Crossing the **soft** ceiling means the next PR that adds material to the file should split it first; `loc-check` prints a warning but stays green. Crossing the **hard** ceiling fails the check: split before merging more functionality.

**The grandfather ratchet.** Files that were already over their hard ceiling when the gate landed are recorded in `scripts/file_size_baseline.json` at their then-current size. They are allowed to stay that big but **may only shrink** -- growing one fails `loc-check`. That is what lets the gate block on day one without demanding a repo-wide refactor first: it stops new debt and stops existing debt getting worse. After you split a baselined file, run `make loc-baseline` to re-record it. Never run it to silence a file you just grew -- the diff makes that obvious in review.

When a file crosses the **soft** ceiling, the next PR that adds material to it should split first. When a file crosses the **hard** ceiling, splitting is required before merging more functionality into it.
Two files are exempt outright (`scripts/check_file_size.py` `_EXEMPT`): `changelog.py` and `commands/context.py` are documentation payloads that happen to live in `.py` files, and a ceiling on them would only push prose out of the repo. Keep that list short -- an exemption is an admission the budget does not model the file.

How to split:
- A client mixing multiple Keboola subsystems (Storage, Queue, Sandboxes, ...) → split by **endpoint family** into a package, e.g. `client/storage_tables.py`, `client/queue.py`, `client/configs.py`, composed into one class via mixins. Keep `BaseHttpClient` shared. (This is exactly what `client.py` -> the `client/` package was in #520.)
Expand Down
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.DEFAULT_GOAL := help

.PHONY: help install install-mcp install-server sync test test-unit test-integration test-e2e test-e2e-local test-e2e-invite test-e2e-feature test-e2e-stream test-file test-cov lint lint-fix format format-check typecheck typecheck-warn skill-check skill-gen version-sync version-check changelog changelog-check check-error-codes parity-check command-sync-check gen-command-reference check clean hooks web-install web-dev-backend web-dev-frontend web-build web-clean
.PHONY: help install install-mcp install-server sync test test-unit test-integration test-e2e test-e2e-local test-e2e-invite test-e2e-feature test-e2e-stream test-file test-cov lint lint-fix format format-check typecheck typecheck-warn skill-check skill-gen version-sync version-check changelog changelog-check check-error-codes loc-check loc-report loc-baseline parity-check command-sync-check gen-command-reference check clean hooks web-install web-dev-backend web-dev-frontend web-build web-clean

help: ## Show this help message
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
Expand Down Expand Up @@ -92,6 +92,15 @@ version-check: ## Check version-bearing files match pyproject.toml (fails if mis
exit 1; \
fi

loc-check: ## Check per-layer file-size budgets in CODE LINES (docstrings/comments excluded)
uv run python scripts/check_file_size.py

loc-report: ## List every module by code lines, largest first
uv run python scripts/check_file_size.py --report

loc-baseline: ## Re-record grandfathered over-budget files (run AFTER a split, never to silence growth)
uv run python scripts/check_file_size.py --update-baseline

changelog: ## Generate changelog skeleton from GitHub releases
uv run python scripts/generate_changelog.py

Expand All @@ -115,7 +124,7 @@ hooks: ## Install git pre-commit hook (lint + format on staged files)
chmod +x .git/hooks/pre-commit
@echo "Pre-commit hook installed."

check: lint format-check typecheck skill-check version-check command-sync-check changelog-check check-error-codes test ## Run all checks (lint + format + typecheck + skill + version + command-sync + changelog + error-codes + test)
check: lint format-check typecheck skill-check version-check command-sync-check changelog-check check-error-codes loc-check test ## Run all checks (lint + format + typecheck + skill + version + command-sync + changelog + error-codes + file-size + test)

clean: ## Remove build artifacts and caches
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
Expand Down
Loading