- **Enforced `PipelineSettings.enabled_components` at resolution time (issue #2116).**
  `is_component_enabled`/`get_enabled_components`
  (`opencontractserver/documents/models.py`) previously only gated the admin
  "Component Library" checkbox UI and a same-mutation-call validation check —
  a component unchecked there still ran at ingest if it remained assigned as
  a MIME default. `get_preferred_parser`/`get_preferred_embedder`/
  `get_preferred_thumbnailer` now return `None` (their existing "not
  configured" contract) for a resolved-but-disabled path;
  `get_preferred_enrichers` filters disabled entries out of the chain instead
  of erroring (enrichment is already best-effort); `get_default_embedder`/
  `get_default_reranker`/`get_default_file_converter` return `""` (their
  existing "disabled" contract) for a disabled default. The documented
  backward-compatible default is unchanged: an empty `enabled_components`
  list still means "all components enabled."
- **Closed a validation gap in `UpdatePipelineSettingsMutation`** (`config/graphql/pipeline_settings_mutations.py`):
  the "assigned components must be a subset of `enabled_components`" check
  previously ran only inside the `if enabled_components is not None:` branch,
  so a call that assigned a *new* `preferred_parsers`/`preferred_embedders`/
  `preferred_thumbnailers`/`default_embedder`/`default_file_converter` value
  without also re-sending `enabled_components` skipped the check entirely,
  even though a prior save had already set a non-empty `enabled_components`.
  Extracted the subset check into `_find_disabled_but_assigned()` and run it
  whenever any of those six fields changes in the call. Added coverage to
  `opencontractserver/tests/test_pipeline_settings.py` — both the getter-level
  enforcement and the mutation-level validation gap
  (`EnabledComponentsMutationTestCase`).
