# Planner

{% tabs %}
{% tab title="End users" %}
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 `adds` and `removes` per feature.
* If a snapshot looks wrong, guardrails may block removals.
* Planning can look “too simple”. That is intentional.

#### If a plan looks wrong

1. Check snapshot quality and staleness.
2. Check guardrails and “mass delete blocked” events.
3. Check your pair direction and scope.

Related:

* Where inventories come from: [Snapshots](/blueprint-architecture/orchestrator/snapshots.md)
* Why deletes get blocked: [Guardrails](/blueprint-architecture/orchestrator/guardrails.md)
* Where the plan gets executed: [Applier](/blueprint-architecture/orchestrator/applier.md)
  {% endtab %}

{% tab title="Power users" %}
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](/blueprint-architecture/orchestrator/one-way-sync.md) and [Two-way sync](/blueprint-architecture/orchestrator/two-way-sync.md)

***

### 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)`

Used for:

* `watchlist`
* `history` (presence-style)
* `playlists` (presence-style item membership)

Algorithm:

1. Compute key sets:
   * `src_keys = set(src_idx)`
   * `dst_keys = set(dst_idx)`
2. Adds are keys in src but not in dst:
   * `adds_keys = src_keys - dst_keys`
3. Removes are keys in dst but not in src:
   * `rem_keys = dst_keys - src_keys`
4. 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)`

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) or `user_rating`
* `rated_at` or `user_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_at` matches 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#s01e02`
* `tmdb: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`)
* `ids` with at least one stable ID token

Strongly preferred:

* `title`
* `year`
* provider 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](/blueprint-architecture/orchestrator/snapshots.md)
* Where diffs get filtered and applied: [One-way sync](/blueprint-architecture/orchestrator/one-way-sync.md), [Two-way sync](/blueprint-architecture/orchestrator/two-way-sync.md)
* Deletion protections: [Guardrails](/blueprint-architecture/orchestrator/guardrails.md)
* Write execution: [Applier](/blueprint-architecture/orchestrator/applier.md)
  {% endtab %}
  {% endtabs %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wiki.crosswatch.app/blueprint-architecture/orchestrator/planner.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
