---
description: "Internal  -  cross-platform parity cross-check for Phase 4 and /multi-agent:review."
---

# Platform parity  -  compare the change against the other platform's repo

A feature that exists on iOS and Android is written twice, and the two copies
drift. The drift is invisible from inside one repo: the iOS diff is
self-consistent, the tests pass, and nobody notices that the Android screen
calls a different endpoint, sends one parameter fewer, or reads a localization
key that no longer exists on the other side.

This step reads the counterpart repo and reports what differs. It is advisory:
it never blocks, never edits, and its silence is never evidence.

## When it runs

The counterpart is **resolved automatically**. Nothing is asked in the normal
case, because the answer is the same on every run of the same project and a
question asked every time is a question people learn to dismiss.

Resolution order, first hit wins. `<slug>` is the primary project's prefs key,
and `MIRROR` is `android` when the run's stack is `ios`, `ios` when it is
`android` - the check runs in both directions and neither platform is the
default one.

| # | Source | Notes |
|---|---|---|
| 1 | `--with <path\|owner/repo>` (`/multi-agent:review` only) | An explicit override for a one-off. Remembered like any other resolution. |
| 2 | `prefs.projects[<slug>].counterpartRoots[]` | What a previous run learned. Verified before use: the path must still exist and its markers must still resolve to `MIRROR`, otherwise the entry is dropped and detection continues at 3. A remembered answer that has gone stale is worse than none. |
| 3 | Sibling checkouts of the primary repo | List the primary checkout's **parent directory**, depth 1, keep the git repos, resolve each one's stack from its marker files. This matches how the repos are actually laid out - a product's iOS and Android checkouts sit next to each other - and it is one directory listing, not a scan of `$HOME`. |
| 4 | `state.siblings[]` | Anything the Phase 0 dev-context picker recorded, in case the counterpart is a submodule or a repo the user selected by hand. |

Then:

- **Exactly one candidate resolves to `MIRROR`** → use it, write it to
  `counterpartRoots[]`, and say which repo was picked in the section header.
  No question, this run or any later one.
- **More than one** → interactive runs ask once, and remember the answer;
  **autopilot and non-interactive runs skip** and record why. An unattended run
  must not block on a picker, and picking one of several by guessing is how a
  cross-repo reader ends up at the wrong tree.
- **None** → skip silently. No section, no placeholder, no prompt, and the run
  is otherwise unaffected. A project with no counterpart repo - most projects -
  never sees this feature exist.

Stack comes from marker files (`.xcodeproj` / `Package.swift` → `ios`,
`build.gradle(.kts)` → `android`), the same table Phase 1 Step 2 owns.
Never guess a stack from the repo name: `my-app-android` is a naming
convention, not a marker, and a wrong stack sends a cross-repo reader at the
wrong tree.

The second condition is the diff: it has to touch something with a counterpart
worth checking - a screen, a service/repository/use-case, a request model, or a
localization file. Otherwise the step is skipped even when a counterpart exists.

Nothing is ever cloned. A counterpart that is not checked out locally is
skipped: cloning a repository in order to review a different one is a side
effect nobody asked for.

## Read-only, without exception

The counterpart repo is **read**. Never edited, never staged, never committed,
never pushed, never branched, and never built. The parity gap belongs to the
other platform's team and their backlog, not to this PR.

This is the `_dev-context.md` read-only sibling contract, applied to Phase 4:
the primary repo is where the work happens, and a finding here is information
for a human, not a task for the pipeline.

## Locating the counterpart, deterministically

Do not grep the other repo by hand and do not read it broadly. Use the code
graph, which is LLM-free and token-budgeted:

```bash
SIB_STACK=android                      # or ios, mirrored
SIB_GRAPH="$HOME/.claude/knowledge/$(basename "$SIB_ROOT")/code-graph.json"

# Build once per repo; refresh only when HEAD moved past the graph's baseCommit.
node "$HOME/.claude/scripts/graph-build.mjs" --root "$SIB_ROOT" --stack "$SIB_STACK" --out "$SIB_GRAPH"

# One budgeted query per feature name taken from the diff. Budget is the point:
# an unbounded dump of the other repo is the context-stuffing this replaces.
node "$HOME/.claude/scripts/graph-query.mjs" "<feature or screen name>" \
  --graph "$SIB_GRAPH" --budget 1500 --json
```

Feature names come from the changed paths and changed type names in the primary
diff, not from the task title - a title says what someone wanted, a path says
what changed.

**Cap: at most 8 counterpart files are read, and the step stops there.** A
change that spans more than that is reported as partially compared, naming what
was left out. A truncation nobody sees is worse than a smaller answer.

## What is compared

Four axes, in this order. Each finding names both sides with `file:line`, or it
is not reported.

| # | Axis | The question |
|---|---|---|
| 1 | Services | Does the counterpart screen call the same endpoints? An endpoint one side calls and the other does not is the finding - including a service that exists only on one platform. |
| 2 | Parameters | Same request fields, query parameters and headers, with the same optionality. A field sent by one side and omitted by the other is a finding even when both requests succeed. |
| 3 | Business rules | The conditions around the call and the response: validation, eligibility, retry, empty and error handling, feature-flag checks. A rule present on one side only is the finding. |
| 4 | Localization keys | The keys the changed screen uses on each side. Different key for the same string, a key one side has and the other does not, and a key that resolves to different copy. |

Nothing else. Layout, naming, architecture and idiom differ between the
platforms by design, and reporting them buries the four things that matter.

## What a finding may not claim

**The extractor's silence is not evidence of absence.** The code graph is
built by deterministic regex rules, so a counterpart it did not find may exist
under a name the rules do not match. A finding therefore reads "no Android call
to `/v1/checkin` was found in the 6 files compared", never "Android does not
call `/v1/checkin`".

When the counterpart screen cannot be located at all, say that, and stop. A
parity report against a file set that is not the counterpart is worse than no
report.

## Output

One section, after the review findings and before the triage notes:

```
## Platform parity  -  <counterpart repo> (advisory, read-only)

Compared: <N> files against <primary screen/feature>.
Not compared: <what was left out, or "nothing">.

1. Services   - iOS calls POST /v1/checkin/seat (SeatService.swift:88); no
                counterpart call found in the 6 files compared.
2. Parameters - both call POST /v1/checkin, iOS sends `cabinClass`
                (CheckinRequest.swift:31), Android does not
                (CheckinRequest.kt:24).
3. Rules      - iOS blocks the call for infants (SeatViewModel.swift:142);
                no equivalent guard found on the counterpart.
4. Keys       - iOS `checkin.seat.title` (Localizable.strings:410) vs
                counterpart `checkin_seat_header` (strings.xml:88).
```

Empty on every axis → one line: `Platform parity: no differences found across
the 4 axes in <N> files compared.` The step never prints an empty section.

## Severity

Parity findings are never blocking. They do not enter the blocking severity
ladder, do not set `review_blocking`, and do not stop a commit or a PR. Triage may summarize them;
it may not promote one to blocking. A platform gap is a planning decision for
two teams, and a reviewer that halts a correct iOS PR over an Android omission
is a reviewer people turn off.
