Planner
Diff engine that compares two snapshots and produces add/remove plans.
Planner decides what CrossWatch will change.
It compares the source and destination snapshots.
It outputs a plan: items to add and items to remove.
What you will notice
Runs show planned
addsandremovesper feature.If a snapshot looks wrong, guardrails may block removals.
Planning can look “too simple”. That is intentional.
If a plan looks wrong
Check snapshot quality and staleness.
Check guardrails and “mass delete blocked” events.
Check your pair direction and scope.
Related:
Where inventories come from: Snapshots
Why deletes get blocked: Guardrails
Where the plan gets executed: Applier
Planner is the orchestrator’s diff engine.
It compares two indices and produces an action plan.
Code: cw_platform/orchestrator/_planner.py
Used by: One-way sync and Two-way sync
Inputs and outputs
Input: indices
Planner consumes SnapIndex dicts:
src_idx: dict[str, dict](source / “desired”)dst_idx: dict[str, dict](destination / “current”)
Keys are canonical keys (usually imdb:..., tmdb:..., etc.).
Output: item lists
Planner outputs lists of item dicts:
adds: item dicts to add (or upsert for ratings)removes: item dicts to remove (or unrate for ratings)
The orchestrator later filters, blocks, chunks, and applies them.
Presence diff: diff(src_idx, dst_idx)
diff(src_idx, dst_idx)Used for:
watchlisthistory(presence-style)playlists(presence-style item membership)
Algorithm:
Compute key sets:
src_keys = set(src_idx)dst_keys = set(dst_idx)
Adds are keys in src but not in dst:
adds_keys = src_keys - dst_keys
Removes are keys in dst but not in src:
rem_keys = dst_keys - src_keys
Convert keys to items:
adds = [src_idx[k] for k in adds_keys]removes = [dst_idx[k] for k in rem_keys]
That is it.
Planner assumes correctness comes from:
canonical key normalization
snapshot coalescing and rekeying
guardrails for deletions
Ratings diff: diff_ratings(src_idx, dst_idx)
diff_ratings(src_idx, dst_idx)Ratings are different.
Presence is not the main thing.
The rating value is.
Planner treats a rating write as an upsert.
Expected rating fields
It tries to read these fields from items:
rating(int 1–10) oruser_ratingrated_atoruser_rated_at(ISO string preferred)
Providers often normalize into a common shape before diffing.
Output meaning
adds: upserts (set rating / update rating)removes: unrates (remove rating)
Algorithm (conceptual)
For each key present in src_idx:
if key not in
dst_idx→ upsert (add)else:
compare rating values
if different → upsert (add)
For keys present in dst_idx but not src_idx:
unrate (remove)
Equality test
Two rating entries are equal if:
rating values match, AND
(optionally)
rated_atmatches when both exist
The planner is tolerant when timestamps are missing.
This yields a “minimal writes” plan.
Episode/season token helpers (planner-adjacent)
Planner diffs by canonical key only.
The pipeline does extra work around TV tokens.
Typed tokens look like:
tvdb:123#s01e02tmdb:456#season:1
This prevents key-mismatch duplicates from turning into noisy add/remove cycles.
That logic lives in the pipelines, not in planner.
Item “minimality” contract
Planner outputs item dicts pulled from the indices.
For correctness, items should include at minimum:
type(movie|show|episode|season)idswith at least one stable ID token
Strongly preferred:
titleyearprovider payload under
item[provider_name_lower]when required for writes
If items are missing IDs, you get:
weak canonical keys
worse deduping
higher unresolved rates
Determinism and ordering
Planner does not preserve input order.
It builds sets, then lists.
If you need stable ordering for logs, sort after planning.
Why planner is “too dumb” on purpose
The orchestrator keeps planning cheap and transparent.
It keeps intelligence in:
snapshot normalization (canonical keys, coalescing)
guardrails (drop guard, mass delete, tombstones)
apply confirmations (unresolved, optional verify-after-write)
failure suppression (blackbox, phantom guard)
If planning gets clever, debugging gets painful.
So planner stays boring.
Related pages
Key normalization: Snapshots
Where diffs get filtered and applied: One-way sync, Two-way sync
Deletion protections: Guardrails
Write execution: Applier
Last updated