Imported from humlab-sead/sead_shape_shifter (
backend/AGENTS.md). Install upstream withnpx skills add humlab-sead/sead_shape_shifter --skill backend. Copyright stays with the author.
Shape Shifter – Backend (backend/) Agent Rules
Rules here apply when working in backend/. Also read the root AGENTS.md for cross-cutting rules.
Layer Boundary
- API models live in
backend/app/models/. Domain logic lives insrc/. Never importbackend.*fromsrc/. - All API↔Core conversions go through
ProjectMapper.to_core()— never bypass it. - Directives (
@include:,@load:,@value:,${ENV_VAR}) are resolved only inProjectMapper.to_core(). The API and YAML layers receive raw strings. ValidationServiceowns loading, resolving, and delegating — it never constructsTargetModelor domain objects directly.
Dependency Injection
- Break circular imports with constructor injection or
TYPE_CHECKINGguards — never by restructuring import order. - Pass the
DataFetchStrategytoDataValidationOrchestratorvia its constructor — never hard-code which strategy to use. - Three fetch strategies:
PreviewDataFetchStrategy,FullDataFetchStrategy,TableStoreDataFetchStrategy— choose based on context (preview → sample rows, full → complete run, table store → in-memory).
Validation Orchestrator (backend/app/validators/)
DataValidationOrchestratorlives in the backend — it is the only component that may call infrastructure (HTTP, file I/O).- Pure domain validators (
src/validators/) receive DataFrames; the orchestrator fetches and passes them. - Returns
list[ValidationIssue]; caller converts to API errors. - Run structural and constraint validation before data validation — structural errors may prevent data loading.
Materialization
- Lifecycle:
CanMaterializeSpecification→ShapeShifter.normalize()→_sanitize_materialized_dataframe()→ persist → update YAML. - Always call
_sanitize_materialized_dataframe()before persisting — removes_merge_indicator_*columns and duplicate labels. - Entity
typedoes not change after materialization — checkis_materialized, nottype. - Unmaterialization: delete sidecar file (best-effort — log warning if missing, do not raise), clear
materializedblock from YAML. - Sidecar path:
materialized/{entity_name}.{extension}relative to project folder — always use_get_materialized_file_path().
Reconciliation
src/reconciliation/is pure domain — no HTTP, no DB. Backend orchestration goes inbackend/app/services/reconciliation*.- Use
determine_strategy()to pickTARGET_ENTITY,ANOTHER_ENTITY, orSQL_QUERY— never hard-code strategy selection. auto_accept_threshold≥review_threshold— useupdate_thresholds()to change both atomically.- OpenRefine client is a shared async singleton — do not create a new
httpx.AsyncClientper request.
Target Model Conformance
TargetModelValidatoris the only component that convertsConformanceIssue→ValidationErrorand touchesValidationCategory.CONFORMANCE.- New conformance validators: decorate with
@CONFORMANCE_VALIDATORS.register(key="...")and extendConformanceValidator. - A project without
metadata.target_modelis not an error — return an empty validValidationResult.
Backend Feature Checklist
When adding a new backend feature, update all four layers:
- Endpoint function in
backend/app/api/v1/endpoints/ - Request/response models in
backend/app/models/ - Business logic in
backend/app/services/ - Router registration in
backend/app/api/v1/api.py
Testing
TestClientfor route/endpoint tests; mockProjectServiceandShapeShifterin unit tests.@pytest.mark.asynciofor all async service and orchestrator tests.- Test each fetch strategy independently (mock the underlying service).
- Test
CanMaterializeSpecificationpreconditions: fixed entity (fail), already-materialized (fail), non-materialized dep (fail). - SIMS client:
backend/app/clients/sims_client.py— env varSHAPE_SHIFTER_SIMS_SERVICE_URL.