papyrus/api/routes: FastAPI routers and HTTP-facing request handling.papyrus/services: service-layer business logic. Prefer new domain modules here instead of growing route handlers.papyrus/schemas: Pydantic request and response models.papyrus/models: SQLAlchemy models and metadata exports used by Alembic.papyrus/config.py: validated environment configuration.papyrus/core: database, exceptions, security and shared infrastructure.alembic: Alembic environment and migration revisions.tests/api/routes: endpoint behavior and contract tests.tests/services: service-layer tests.tests/conftest.py: shared fixtures and test database setup.
When working in the Papyrus workspace, read ../CODING_STYLE.md for the
maintainer's reviewed examples. The rules below also apply in standalone checkouts.
- Use meaningful local and callback names; avoid single-letter operands and redundant qualifiers when a clear role name is available.
- Prefer clear branches or named predicates to nested conditional expressions and substantial inline validation conditions. Preserve predicate polarity and null handling when extracting helpers.
- Extract meaningful constants and focused helpers for distracting literals or construction details. Preserve types, scope, return values, and behavior.
- Keep Ruff's 120-character line length. Use trailing commas for deliberate multiline argument layouts, and give multiline model/schema fields breathing room while keeping related simple fields together.
- Remove documentation that merely repeats a declaration and decorative section banners. Retain substantive contracts and constraints in docstrings.
- Keep route handlers thin. They should parse input, enforce dependencies/auth, call a service, translate errors, and return schemas.
- Put business rules, query orchestration, and transaction-aware logic in
papyrus/services, not in route modules. - Reuse or extend existing schema modules before creating new top-level packages.
- Follow existing async patterns with
AsyncSession, explicit return types, and Pydantic schemas. - Keep consecutive single-line statements together. A change in purpose does not require a blank line, including query execution, result extraction, assignments, persistence calls, and returns.
- Treat any multiline statement or block as a visual boundary inside a function. If one adjacent statement is multiline and both sides are real code, separate the two statements with a blank line.
- Apply that rule to multiline conditionals, loops,
withblocks,tryblocks, multiline calls, multiline literals, and multiline return values. Treatif/elif/elseas one block. - Do not add a blank line just because a docstring appears above the first statement in a function. The multiline-block rule applies between code statements, not between a docstring and the first line of code.
- Do not pad the start or end of a block. Spacing follows blocks and multiline statements, not inferred logical phases or side effects.
- When adding a new router module, register it in
papyrus/api/routes/__init__.py. - Avoid adding dependencies unless the user explicitly asks for them.
- Keep changes scoped. Do not refactor unrelated areas as part of a focused fix.
- Do not leave inline comments. Sufficiently complex behavior that requires explanation should be documented using docstrings.
- Install dependencies:
uv sync --extra dev - Start Postgres:
docker compose up -d database - Stop services:
docker compose down - Run the API locally:
uv run uvicorn papyrus.main:app --reload - Apply migrations:
uv run alembic upgrade head - Create a migration:
uv run alembic revision --autogenerate -m "<message>" - Run all tests:
uv run pytest - Run integration tests:
uv run pytest -m integration - Run one test module:
uv run pytest tests/api/routes/test_<module>.py - Lint:
uv run ruff check . - Format:
uv run ruff format . - Typecheck (installed, configured, and used in CI):
uv run --locked mypy .
- Add or update tests for every behavior change.
- Prefer route tests for HTTP contract changes and service tests for business logic.
- If database behavior changes, add the narrowest regression test that proves the query or transaction behavior.
- Mark schema-changing tests with
pytest.mark.migration. Ordinary tests share the schema and truncate rows after each case; migration tests rebuild it before and after each case, including failures. - Use
public_clientorpublic_debug_clientfor routes that do not access the database. These fixtures reject accidental database access. - Set
PAPYRUS_TEST_TIMING=/tmp/pytest-timing.jsonto export setup, call, and teardown timings. CI publishes these as a job summary and artifact. - Do not claim success without running the most relevant checks, or explicitly stating why they were not run.
- Add an Alembic revision for every schema change, constraint change, index change, or persisted-data backfill.
- Update SQLAlchemy models first, then review autogenerated Alembic output before committing it.
- Make sure new or changed models are imported from
papyrus.modelsso Alembic metadata can see them. - Keep migrations small and pair them with the application code that depends on them.
- Do not ship destructive or irreversible migrations without explicit user approval.
- Relevant tests pass, or any unrun checks are called out with the reason.
- Ruff passes on the changed scope.
- Typechecking is run on the changed scope using the repo's configured Mypy setup.
- Schema changes include a migration and verification.
- New backend behavior follows thin-route and service-layer separation.
- No new dependencies were added unless explicitly requested.
Local auth testing supports Mailpit for SMTP capture, a dev auth sandbox at /__dev/auth-sandbox, and opt-in provider smoke tests.
Use .env.example, tests/api/routes/test_auth.py, and
tests/integration/test_auth_smoke.py for current configuration and test entry points.
See README.md for current auth/sync routes, local sandbox entry points and test lanes.
For client/server contracts, use the workspace's papyrus-sync-contract skill and
../.agents/skills/papyrus-sync-contract/references/CONTRACT_MAP.md when developing
inside the Papyrus workspace.
To build the dev sandbox assets without the Vite dev server:
npm --prefix frontend/dev-pages run buildWhen checked out inside the Papyrus workspace, run from its root:
tools/papyrus deps serverinstalls dev tools withuv sync --locked --extra dev.tools/papyrus check serverruns Ruff lint, non-mutating format checks, and Mypy.tools/papyrus test server -- tests/services/test_sync.pyruns focused tests.
The shared fixtures truncate test rows and rebuild tables for migration tests. Most route tests need PostgreSQL
without being marked integration. Use a separate local test database, never the
application database; do not run suites concurrently on the same database. The CLI
checks the database target and holds a workspace test lock. External auth_smoke
tests are excluded by default and require explicit provider configuration.
Sync uploads in papyrus/services/sync.py lock per owner, commit mixed batches
atomically, and delete physical media after commit. library_sync.py enforces
ownership and tombstones so delayed offline writes cannot resurrect deleted rows.
Preserve those semantics when changing persistence or upload validation.