Contributing¶
Dev loop¶
uv venv
uv pip install -e ".[dev]"
cp .env.example .env
uv run alembic upgrade head
uv run pytest -q
uv run uvicorn sfmapi.runtime:create_app --factory --reload
Running tests under both DB engines¶
bash scripts/test_dual_db.sh # SQLite + (Postgres if SFMAPI_DB_URL_PG set or docker available)
bash scripts/test_postgres_local.sh # ephemeral Postgres in docker
Lint + type¶
uv run ruff check app tests
uv run ruff format --check app tests
uv run mypy app
Smoke-testing the deploy¶
bash scripts/smoke.sh # bring up compose, walk API, tear down
bash scripts/smoke.sh --keep # leave stack up on success
Conventional commits¶
Commit titles drive the changelog (release-drafter). Use:
Prefix |
Maps to release-drafter category |
|---|---|
|
🚀 Features |
|
🐛 Fixes |
|
⚡ Performance |
|
🛠 Internal |
|
📦 Dependencies |
|
📚 Docs |
|
🤖 CI |
|
💥 Breaking |
scripts/smoke.sh and the dual-DB tests are the merge gates; if
either is red, the PR doesn’t land.
Adding a new endpoint¶
Pydantic schema under
app/schemas/api/.Service function under
app/services/.Route under
app/api/v1/, mounted fromapp/main.py.Test under
tests/e2e/(andtests/integration/if it touches storage).Update the API reference.
If the route submits a Task, see “Adding a new SfM stage” below.
Re-run
uv run python scripts/regen_sdk.pyso all three SDKs pick up the new endpoint.
The web tier must not import any engine library (pycolmap, torch,
cv2, segment_anything, …); the
test_app_does_not_import_pycolmap_or_torch unit test enforces that
boundary.
Adding a new SfM stage¶
The most common contribution shape — a new pipeline stage like
pgo, triangulate, or mesh. The post-extraction DX is designed
so the touchpoints are minimal and the drift modes are caught
mechanically.
Add the worker task at
app/workers/tasks/<kind>.py. Decorate the entry function:from sfmapi.backends import require_backend_method from app.workers.tasks._registry import task_handler @task_handler("my_stage") def run(task: Task) -> dict: inputs, spec = read_state(task) backend = get_backend() method = require_backend_method( backend, "my_stage_method", capability="my_stage.canonical", ) result = method(...) return {"result_path": ..., **result}
Auto-discovery picks it up — no edit to
app/workers/dispatcher.pyneeded. The decorator raises on duplicate-kind so typos collide loudly.Add the capability string to
app/core/capabilities.py::ALL_KNOWN. Pick a canonical name (pgo.optimize,mesh.poisson,<family>.<variant>).Add a Protocol method to the smallest matching protocol in
app.adapters.backendif the stage needs a new backend-side operation. KeepSfmBackendas the full union for complete engines. Add the matching stub method toapp.adapters.stub_backend.StubBackendonly when the no-op stub must satisfy that protocol.Add a service helper in
app/services/sfm_stage_service.py:async def submit_my_stage(...) -> tuple[str, list]: require_capability("my_stage.canonical") # MUST be in ALL_KNOWN # ... assemble inputs dict ... return await _submit_single_stage( session, tenant_id=tenant_id, project_id=..., recipe="my_stage", kind="my_stage", inputs=inputs, spec=spec, inline=inline, )
The capability-consistency test (
tests/unit/test_capability_consistency.py) AST-scans forrequire_capability("X.Y")literals and fails if"X.Y"is missing fromALL_KNOWN.Add a route in the appropriate
app/api/v1/<resource>.py, delegating to the service helper. Useaccepted_response(JobAcceptedResponse(...))for the 202 envelope.Add tests: at minimum an e2e test that POSTs the route and inspects the resulting
Job.status(the inline queue runs the task in-process, so a single test exercises the full route → service → worker → backend path).Update
SFMAPI-SPEC.md§6 with the new endpoint, tagged[Extension: <capability>]if it’s optional or[Core]if every conformant server must implement it. Updatedocs/reference/api.mdwith the route catalog entry.Regen SDKs with
uv run python scripts/regen_sdk.py. The contract tests will replay the new fixture through Python + TypeScript + C++ on next CI run.
Adding a new backend or backend method¶
sfmapi ships no concrete SfM backend; engine packages live in their own repos and satisfy the smallest protocol layer they need.
Implement
Backendfor identity, portable capabilities, and runtime versions. Add optional stage protocols such asFeatureBackendorMappingBackendonly when those portable stages really work. Action-only backends do not need placeholder stage methods.Keep
capabilities()portable. Backend-native commands such ascolmap.feature_extractororopenmvg.compute_featuresbelong inlist_backend_actions(), not inALL_KNOWN.Register the factory at app startup:
sfmapi.runtime.register_backend("name", MyBackend).Add a backend contract test:
assert_backend_contract(MyBackend())fromsfmapi.backends. This catches unknown portable capabilities, malformed action/config descriptors, duplicate ids, non-portablerequired_capabilities, runtime-managed options inbackend_optionsschemas, and action/config ids leaked throughcapabilities(). For a package-level smoke check, runsfmapi check-backend --import my_backend --backend my_backend.If a new wire op is needed (a method not yet on a protocol), add it to the narrowest protocol in
app/adapters/backend.pyand surface a worker task underapp/workers/tasks/(see “Adding a new SfM stage” above). Worker tasks call backends throughget_backend()plusrequire_backend_method(...), never via direct import.
Backends advertising a capability that is not in
app.core.capabilities.ALL_KNOWN will see that capability silently
dropped at detect_capabilities time (logged as a warning); add the
canonical name to ALL_KNOWN first only when it is a portable sfmapi
feature. For engine-native tools, add or fix the backend action
descriptor instead.