Backfill model_registry/experiments access levels to PRIVATE on existing non-public projects

Goal

Batched background migration that sets model_registry_access_level and model_experiments_access_level to PRIVATE on existing non-public projects where these values are currently ENABLED.

Filter:

project.visibility_level != PUBLIC AND model_*_access_level = ENABLED

PRIVATE rows are left alone (already members-only). DISABLED rows are left alone (feature is deliberately off). PUBLIC is not a reachable state for these features — ProjectFeature#allowed_access_levels rejects it via validation (only pages and package_registry are excluded from that check), so no PUBLIC rows are expected to exist.

Why

Today, the ProjectPolicy carve-out

rule { ~public_project & guest & model_registry_enabled }.enable :read_model_registry

forces member-only access for model registry / experiments on non-public projects regardless of the Featurable access level. Once the carve-out is removed (Task C), the Featurable access level becomes the source of truth. This migration sets the existing population to the access level that reproduces today's observable behavior.

Sequencing

Blocked by Task A (dynamic defaults). Must not deploy until A is live, because any non-public project created in the gap between B and A would still get the static ENABLED default and would regress when the policy carve-out is later removed.

References

Edited by Ian Anderson