Project Checks
This document explains polepos check for both human users and coding agents.
polepos check is the project contract validator for PolePosition-generated
FastAPI projects. It answers one practical question:
Can this project still be grown safely with PolePosition lifecycle commands?
It does not connect to the database, start FastAPI, call Kafka, call RabbitMQ, call Redis/RQ, or contact an LLM provider. It reads files and reports structural drift.
User Guide
Run the command from the project root or any nested directory inside a PolePosition project:
polepos check
polepos check --json
polepos check --fix
Run it after:
- creating a project with
polepos start - adding a module with
polepos add module - removing a module with
polepos remove module - adding Kafka, RabbitMQ, Redis, or RQ with
polepos add integration ... - adding auth with
polepos add auth - manually editing
.poleposition.toml,api/router.py,db/models.py,modules/__init__.py,settings.py,.env.example, orpyproject.toml - resolving merge conflicts in generated or managed files
- before opening a pull request or handing a project to another agent
Successful output looks like this:
PolePosition project check passed.
Project root: /path/to/myapp
Package: myapp
Failure output lists the project, package, and every issue found:
PolePosition project check failed.
Project root: /path/to/myapp
Package: myapp
Issues:
- [PPCHK034] Lifecycle module 'garage' is missing API router include in ...
Fix: Restore the router include, or clean the detached module with ...
For CI and coding-agent workflows, use --json:
polepos check --json
Successful JSON output looks like this:
{
"issues": [],
"package_name": "myapp",
"passed": true,
"project_root": "/path/to/myapp"
}
Use --fix when only safe managed markers need restoration:
polepos check --fix
--fix restores missing PolePosition markers in managed files, then runs the
same checks again. It does not install packages, generate migrations, or call
external services. In api/router.py, it places the router include marker
after the complete api_router.include_router(...) statement, including
multi-line include calls, so later lifecycle commands can keep inserting new
router wiring safely.
If fixes are applied, text output lists the changed paths before the check result:
Applied fixes:
src/myapp/api/router.py
PolePosition project check passed.
Project root: /path/to/myapp
Package: myapp
With --json --fix, the payload also includes fixed:
{
"fixed": ["src/myapp/api/router.py"],
"issues": [],
"package_name": "myapp",
"passed": true,
"project_root": "/path/to/myapp"
}
Failed JSON output keeps the same process exit behavior as the text output and reports issues as structured objects:
{
"issues": [
{
"code": "PPCHK034",
"message": "Lifecycle module 'garage' is missing API router include in ...",
"remediation": "Restore the router include, or clean the detached module with ..."
}
],
"package_name": "myapp",
"passed": false,
"project_root": "/path/to/myapp"
}
The default command is intentionally diagnostic. It does not rewrite files,
install dependencies, create migrations, or apply fixes. The only mutating mode
is --fix, and it is limited to safe managed-marker restoration.
After polepos remove module, check can verify that generated router,
export, model-import, and test wiring no longer references the removed module.
It does not connect to the database and does not verify whether a removed
module's table still exists. Schema cleanup remains an Alembic migration
decision.
When check reports a removed module, it is reporting file drift, not runtime
state. The usual fix is to restore the missing module directory or run:
polepos remove module <name>
If the remaining reference is custom code, remove or rewire that reference
manually before rerunning check.
Safe Customization Boundaries
PolePosition projects are normal FastAPI projects, so users can and should edit
their application code. The important boundary is the lifecycle contract used by
polepos add module, polepos remove module, polepos add integration ...,
polepos db ..., and polepos check.
Safe customization examples:
- edit generated module
model.py,schemas.py,repository.py,services/<module>_service.py, androuter.pyfor the real domain - add new route handlers inside an existing module router
- add custom helper modules under
src/<package>/ - add reviewed Alembic revisions under
migrations/versions/ - change runtime values in
.env - add application tests under
tests/
Avoid these changes unless the team intentionally opts out of PolePosition lifecycle management for that file:
- removing or renaming
# polepos:*managed marker comments - manually rewriting managed imports, router includes, model imports, or module exports into a shape the CLI cannot recognize
- deleting a generated module directory or generated tests by hand and leaving router, model, export, or test references behind
- moving or renaming generated core files such as
api/router.py,db/models.py,settings.py,.env.example,.poleposition.toml, or Alembic files - adding a SQLAlchemy model without importing it through
db/models.py - creating tables during FastAPI startup instead of using Alembic migrations
- partially adding integration settings or
.env.examplevalues by hand while relying onpolepos add integration ...orpolepos checkto manage the same integration later - commenting out required generated integration values such as
KAFKA_BOOTSTRAP_SERVERSorLLM_PROVIDERwhile expectingcheckto treat them as present - editing
.env.exampleas a secret store; put local secrets in.env
Generated module schema classes and fields are normal application code. You can
rename or replace them, but update the router, service, repository, generated
tests, and any custom imports at the same time. polepos check does not try to
enforce every generated Pydantic class name or every generated field because
those are expected to become domain-specific quickly. For example, if a
customers module changes CustomerCreate to CreateCustomerRequest, update
all imports and type annotations that reference CustomerCreate.
Do not delete generated schema classes or generated fields in isolation while
leaving generated router, service, or tests unchanged. In that case pytest is
the right validation layer: import errors, FastAPI response_model errors,
payload.name attribute errors, and response shape assertion failures are
runtime/test-contract issues rather than lifecycle wiring issues.
If a module was already deleted manually, run
polepos remove module <name> before making more structural edits. The command
can clean generated wiring and tests that still reference the missing module.
After any structural customization, run:
polepos check
After schema, service, router, repository, or model customization, also run the generated pytest suite:
uv run pytest
What It Checks
polepos check currently has three layers.
1. Core Check
Core check validates the generated project foundation:
- project identity
.poleposition.tomlpackage and database mode metadata when present- generated project structure
- Alembic migration files for database-backed projects
- PolePosition-managed markers
Project identity means the command can find one generated application package
under src/ and can report missing core paths instead of simply saying the
directory is unknown.
Core generated paths include the package entrypoints, settings, bootstrap
files, API files, domain files, the status starter module, test conftest,
the generated status integration test, README, AGENTS.md, and
.env.example. Database-backed projects also include generated db/ files.
Alembic paths are checked only when the project has generated database wiring. They include:
alembic.inimigrations/env.pymigrations/script.py.makomigrations/versions
Projects generated with polepos start --db none intentionally omit
SQLAlchemy, Alembic, DATABASE_URL, and generated db/ paths. polepos check
accepts that shape and skips database-specific structure checks.
New generated projects include .poleposition.toml. The file records the
application package, database mode, generated modules, and generated
integrations. Older projects without the file still work through structural
inference. Integration values must use TOML booleans such as kafka = true or
kafka = false; quoted strings such as kafka = "false" are reported as
invalid manifest values instead of being treated as generated integration
signals. CRUD module options are recorded on the module template value, such as
customers = "crud[pagination,timestamps,soft-delete]", so lifecycle commands
can compare generated files against the right template variant. If the manifest
cannot be decoded as UTF-8, check reports it as manifest drift and lifecycle
commands leave it untouched instead of overwriting a corrupt file. If a project
intentionally uses a user-managed database workflow
outside PolePosition's SQLAlchemy/Alembic lifecycle, set:
[poleposition]
db = "custom"
custom tells check not to infer the standard Alembic lifecycle from custom
database settings or dependencies.
Managed markers include:
# polepos:router-imports# polepos:router-includes# polepos:model-imports# polepos:module-exports# polepos:auth-settings# polepos:auth-env# polepos:integration-settings# polepos:integration-env# polepos:llm-settings# polepos:llm-env
These markers are also used by lifecycle commands. If the integration settings
or env markers are removed, polepos add integration ... stops before writing
generated files so the project is not left partially patched.
Generated managed files are expected to be UTF-8 text. If check cannot decode
a managed file, it reports that file as project drift instead of crashing with a
raw decode traceback. Restore the file as UTF-8 text, regenerate the affected
scaffold, or intentionally detach that managed surface.
2. Lifecycle Check
Lifecycle check validates modules added after project creation.
Starter modules are not treated as added modules:
status
Projects generated by older PolePosition versions may still contain the legacy
profile and races sample modules. check treats those legacy sample
modules as starter modules when their original generated signals are present,
so upgraded CLI versions can still validate older generated projects.
Lifecycle check also validates that the starter status module remains wired
into api/router.py without an added router prefix, and reports orphan
generated references when a module directory was deleted manually but
router/model/export/test references remain. Python references are parsed from
the full managed file, so custom imports below PolePosition markers are still
reported when they point at a missing module.
For example, if src/myapp/modules/garage/ is gone, all of these count as
orphan references:
from myapp.modules.garage.router import router as garage_router
api_router.include_router(garage_router, prefix="/garage", tags=["garage"])
from myapp.modules.garage import model as garage_model
Generated wiring can usually be cleaned with:
polepos remove module garage
Custom references are not guessed away. If the project intentionally replaced
the generated router include or model import with a custom line, remove or
rewrite that custom line explicitly before expecting check to pass.
For a standard module, check expects:
- module files:
__init__.py,model.py,repository.py,router.py,schemas.py,services/__init__.py,services/<module>_service.py - export in
src/<package>/modules/__init__.py - router import in
src/<package>/api/router.py - router include in
src/<package>/api/router.py - model import in
src/<package>/db/models.py - integration test under
tests/integration/test_<module>.py - unit test under
tests/unit/test_<module>_service.py
For a crud module, check expects the standard database-backed wiring plus:
- module service file:
services/<module>_crud_service.py - integration test under
tests/integration/test_<module>_crud.py - unit test under
tests/unit/test_<module>_crud_service.py
CRUD feature options such as pagination, timestamps, soft delete, tenant scope, and auth-required routes do not change the required file names, but the manifest keeps the selected option list so other lifecycle commands can recognize the generated content.
For an ai-prompt module, check expects:
- module files:
__init__.py,orchestrator.py,prompts.py,router.py,schemas.py,services/__init__.py,services/<module>_service.py - export in
src/<package>/modules/__init__.py - router import and include in
src/<package>/api/router.py - integration test under
tests/integration/test_<module>.py - unit test under
tests/unit/test_<module>_orchestrator.py
ai-prompt modules are intentionally not required to have a model import in
db/models.py.
For an api-only module, check expects:
- module files:
__init__.py,router.py,schemas.py,services/__init__.py,services/<module>_service.py - export in
src/<package>/modules/__init__.py - router import and include in
src/<package>/api/router.py - integration test under
tests/integration/test_<module>.py - unit test under
tests/unit/test_<module>_api_service.py
api-only modules are intentionally not required to have model, repository, or
db/models.py wiring.
Module Dependency Check
Lifecycle check also validates the dependency graph between added modules and
reports circular dependencies. It scans each src/<package>/modules/<name>
package for imports of <package>.modules.<other>, builds the directed module
graph (using the polepos.data.Graph that ships with the package), and reports
any cycle with the offending path:
- [PPCHK060] Circular module dependency detected: customers -> billing -> notifications -> customers
A cycle means those modules can no longer be reasoned about, tested, or removed
in isolation. Break it so module dependencies form a DAG: extract the shared
code into a separate module, or invert one dependency with an interface or an
event. Imports of shared infrastructure such as <package>.db or
<package>.settings, and third-party packages, are not module edges and never
trigger this check.
Auth Workflow Check
Auth workflow check validates projects that ran polepos add auth or otherwise
contain auth workflow signals. It expects:
- generated auth files such as
auth/model.py,auth/router.py,auth/user_schemas.py, andauth/user_service.py - generated tests under
tests/integration/test_auth.pyandtests/unit/test_auth_service.py - dependency:
pwdlib[argon2]>=0.2.0 - auth router import/include in
api/router.py - auth model import in
db/models.py
The auth workflow requires generated database wiring. Projects created with
--db none must add an explicit database layer before using this workflow.
3. Integration Check
Integration check validates opt-in external-system scaffolds when the project
contains signals that those integrations are present. In projects with
.poleposition.toml, explicit manifest entries and file, dependency, setting,
or active env signals are both treated as integration signals. Commented
settings or env lines do not satisfy required integration values; generated
commented env examples such as optional Kafka compression or LLM token limits
are treated as optional examples.
The distinction is deliberate:
KAFKA_BOOTSTRAP_SERVERS=localhost:9092 # required and active
# KAFKA_BOOTSTRAP_SERVERS=localhost:9092 # required but inactive, reported missing
# KAFKA_COMPRESSION_TYPE= # optional example, allowed commented
The same rule applies to generated settings in settings.py:
kafka_bootstrap_servers: str = "localhost:9092" # present
# kafka_bootstrap_servers: str = "localhost:9092" # commented out, missing
If a required env key was accidentally pre-seeded as a comment before running
polepos add integration kafka, the add command inserts an active generated
line instead of treating the comment as complete.
Kafka is checked when integrations/kafka, Kafka settings, Kafka env values,
or the Kafka dependency are present. check expects:
src/<package>/integrations/kafka/__init__.pyconsumer.pyfactory.pyproducer.pyschemas.pytesting.py- dependency:
aiokafka>=0.12.0 - settings such as
kafka_bootstrap_servers,kafka_client_id, andkafka_request_timeout_ms - env values such as
KAFKA_BOOTSTRAP_SERVERS,KAFKA_CLIENT_ID, andKAFKA_REQUEST_TIMEOUT_MS - optional commented env example:
KAFKA_COMPRESSION_TYPE
RabbitMQ is checked when integrations/rabbitmq, RabbitMQ settings, RabbitMQ
env values, or the RabbitMQ dependency are present. check expects:
src/<package>/integrations/rabbitmq/__init__.pyconsumer.pyfactory.pypublisher.pyschemas.pytesting.py- dependency:
aio-pika>=9.0.0 - settings such as
rabbitmq_url,rabbitmq_exchange, andrabbitmq_prefetch_count - env values such as
RABBITMQ_URL,RABBITMQ_EXCHANGE, andRABBITMQ_PREFETCH_COUNT
Redis is checked when integrations/redis, Redis settings, Redis env values,
or the Redis dependency are present. check expects:
src/<package>/integrations/redis/__init__.pycache.pyfactory.pyschemas.pytesting.py- dependency:
redis>=5.0.0 - settings such as
redis_url,redis_client_name, andredis_key_prefix - env values such as
REDIS_URL,REDIS_CLIENT_NAME, andREDIS_KEY_PREFIX
RQ is checked when integrations/rq, RQ settings, RQ env values, or the RQ
dependency are present. check expects:
src/<package>/integrations/rq/__init__.pyfactory.pyjobs.pyschemas.pytesting.pyworker.py- dependency:
rq>=1.16.0 - settings such as
rq_redis_url,rq_default_queue, andrq_job_timeout_seconds - env values such as
RQ_REDIS_URL,RQ_DEFAULT_QUEUE, andRQ_JOB_TIMEOUT_SECONDS
LLM is checked when integrations/llm, LLM settings, LLM env values, or an
ai-prompt module are present. check expects:
src/<package>/integrations/llm/__init__.pyanthropic_client.pyfactory.pyopenai_client.pyprovider.pyschemas.py- settings such as
llm_provider,llm_model, andllm_api_key - env values such as
LLM_PROVIDER,LLM_MODEL, andLLM_API_KEY - optional commented env example:
LLM_MAX_TOKENS
LLM does not require a provider SDK dependency by default because the generated adapters are provider-agnostic stubs.
How To Read Issues
Each issue includes a stable code, the problem text, and a short remediation hint:
- [PPCHK021] Managed marker '# polepos:router-imports' is missing in ...
Fix: Restore the listed # polepos marker or manage that file manually.
Issue text still names the layer by implication:
Required generated path is missing: core generated structure driftRequired Alembic path is missing: migration setup driftProject manifest has unsupported ...: invalid lifecycle manifest metadataManaged marker ... is missing: a managed insertion point was removedLifecycle module ...: added module wiring driftOrphan module reference ...: generated remnants point at a missing moduleIntegration ...: opt-in integration wiring drift
Issue codes are intended to stay stable enough for humans, coding agents, CI logs, and future machine-readable output. The current code families are:
PPCHK00x: project identityPPCHK01x: generated structure, Alembic, and database-mode driftPPCHK02x: managed file and marker driftPPCHK03x: module lifecycle driftPPCHK04x: integration drift
Fix the project by restoring the expected file, marker, import, dependency, setting, or env value. If a team intentionally opts out of PolePosition-managed updates for a file, treat the check failure as expected drift and document that decision in the project.
Agent And LLM Contract
Coding agents should treat polepos check as a lifecycle contract, not as a
style checker.
When changing generated structure, module generation, integration generation, managed markers, or Alembic behavior:
- update
pole_position/cli/services/project_checker/ - update
pole_position/tests/test_check_command.py - update
pole_position/tests/test_project_checker.pywhen helper behavior changes - update this document
- update README when user-facing behavior changes
Do not make check depend on network access, a running database, a running
message broker, or installed optional integration dependencies. The check layer
should stay fast, file-based, deterministic, and safe to run in CI.
Do not use check to hide FastAPI behavior behind a framework. It should
validate PolePosition-managed project shape while keeping the generated app
explicit and understandable.