Reconcile Hub Content
Materialize content_nodes for a hub’s provenance-scoped playlists (D4).
A bodyless POST (or a body with attributes omitted / playlist_ids
omitted) derives the playlist set from this hub’s OWN
HubTemplateApplication history — the common case, and the intended
default usage for both an operator running this after a data issue and
mio-cli re-running it after its own step sequence. Passing an
explicit playlist_ids list is for the one case provenance cannot
cover (a hub with no application record at all — see the plan’s §6 open
question); every id in that list must belong to THIS hub.
Idempotent by construction: ContentService.ensure_nodes hash-first
resolves every spec (D6), so calling this twice in a row with the SAME
resolved playlist set adopts everything the first call created rather
than creating it again — zero duplicate rows, zero duplicate INSERT
attempts on the second call.
Returns 200 with one result per resolved NodeSpec, in the full D7
outcome vocabulary (created / adopted / adopted_not_member_visible /
skipped_*) — nothing is log-only, so a caller can tell exactly what
happened without diffing the hub.
Error codes
404 hub_not_found — hub_id does not exist, or belongs to
a different team than team_id
(enumeration-safe: both cases 404
identically).
422 no_playlist_provenance — the hub has no HubTemplateApplication
row carrying any playlist ids, AND no
playlist_ids were supplied in the
request body.
422 playlist_not_in_hub — one or more ids in an explicit
playlist_ids list do not resolve to
a Playlist scoped to THIS hub
(meta.invalid_playlist_ids lists
which).
429 — rate limit (60/hr/IP) exceeded.
RAISE, NEVER RETURN: every rejection above is an AppException
subclass (NotFoundError / ValidationError,
app/infrastructure/exceptions.py) raised out of
HubContentReconcileService.reconcile and left to propagate uncaught
— never caught here and turned into a hand-rolled JSONResponse. They
are mapped by the centrally-registered app_exception_handler
(app/main.py), which runs AFTER get_db()’s except-branch has
already rolled back anything ensure_nodes flushed before the
rejection. A handler that instead caught one of these and RETURNED an
error response would return NORMALLY from this route, sending
get_db() down its commit branch instead — durably persisting
whatever partial work happened before the error. See
app/hub_scaffold/reconcile_service.py’s module docstring for the
same contract stated from the service side.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
Response
Successful Response