# Two-pass synthesis (Phase 2, 2a, 2b)

> Two-pass synthesis: the platform-agnostic concept pass and the per-platform Pass B projection, plus the convention preview that gates it. Loaded on demand by `/multi-agent:analysis` and by pipeline Phase 1.

### Phase 2 - Two-pass synthesis (per-platform)

Synthesis runs in two passes: Pass A computes the shared logical content once, Pass B projects it onto each selected platform.

#### Pass A - Shared synthesis (sequential, fail-soft)

Build one in-memory `synthesizedSections` object from all evidence. Apply the omission rule per section (zero-evidence → `null`). The object holds both shared content (Sections 1, 4) and `byPlatform` projections (Sections 2, 3, 5, 6, 7).

```
synthesizedSections = {
  scope: { ... shared block ... },
  design: { shared: {frameGallery, codeConnect}, byPlatform: {ios, android, frontend} },
  localization: { keys: [...], byPlatform: {ios: {source: "Localizable.strings", rows}, android: {source: "strings.xml", rows}, frontend: {source: "i18n/*.json", rows}} },
  apiContracts: { ... shared verbatim ... },
  deeplinkPush: { byPlatform: {ios, android, frontend, backend: null} },
  business: { rules: [...], useCases: [...], firebaseEvents: [...], byPlatform: {ios: {testFramework: "Swift Testing", skeletons}, android: {testFramework: "JUnit5 + Turbine"}, frontend: {testFramework: "Vitest + RTL"}, backend: {testFramework: "pytest"}} },
  devPlan: { byPlatform: {ios: {tasks, standards}, android: {...}, ...} }
}
```

| Section | Production logic |
|---------|------------------|
| 1. Scope | Always present. Derive in-scope / out-of-scope, audience from Jira summary + Confluence title + feature name. LLM inference allowed. Shared across all per-platform files verbatim. |
| 2. Design | If `evidence.figma[]` is empty AND no `evidence.repoEvidence[*].buckets.uiComponents` direct-match → `byPlatform[*]` = null. Otherwise produce frame gallery (shared) + per-platform component inventory (rows from `evidence.repoEvidence[<repo>].buckets.uiComponents`). |
| 3. Localization | If both Figma annotations/text layers and `evidence.repoEvidence[*].buckets.localizationKeys` are empty → `byPlatform[*]` = null. Otherwise produce the key table per the project `localization.ownership` mode (Locked 20): `in-repo` fills the configured locale set (default `ar, de, en, es, fr, it, ru, tr`, RTL for ar); `externally-owned` lists key + status + copy source + base value and defers per-locale values. Base copy is sourced from the Figma annotation when present (Locked 3). |
| 4. API Contracts | If `evidence.swagger[]` is empty AND no Confluence embedded API table AND no `evidence.repoEvidence[*].buckets.services` direct-match → null. Otherwise produce endpoint summary + per-endpoint tables (shared verbatim across files). |
| 5. Deeplink / Push | Per-platform: iOS uses Universal Links / `UNUserNotificationCenter`; Android uses `intent-filter` / FCM; Frontend uses web URL routing; Backend file omits this section entirely. |
| 6. Business + Tests | If sections 2 and 4 are both `null` AND `evidence.firebase[]` is empty → null. Otherwise produce use-case + mock stubs + per-platform test skeletons + shared Firebase events table. Reuse Red-Green-Refactor naming from `$HOME/.claude/rules/tdd.md`. |
| 7. Development Plan | Always present. Tasks for the current platform only. Architecture standards come from `evidence.standards[]` filtered by platform (see Pass B step 1). **Reuse-first rule (Locked 11)**: when an item has `direct-match` in `evidence.repoEvidence[<repo>].buckets.<X>`, emit `Reuse existing <FQN> (<file>:<line>)` instead of `Add new <FQN>`. New-write task with a `direct-match` competitor becomes a Risk row. |

#### Phase 2a - Pass B preview (Locked 26)

Before Pass B renders any file, present the resolved convention table to the user via `AskUserQuestion`. The table is one row per concept, one column per selected platform.

Example (iOS + Android selected):

```
Convention preview - Pass B will render with:

| Concept | iOS | Android |
|---|---|---|
| Module folder | Domains/Profile/.../Screens/ProfileEditor/ ^[C1 high] | feature/profile/.../userprofile/ ^[C1 high] |
| State holder | UserProfileViewModel ^[C2 high: 8 examples] | UserProfileViewModel ^[C2 high: 5 examples] |
| State model | sealed enum UserProfileUIState ^[C3 high] | sealed interface UserProfileUiState ^[C3 high] |
| Test naming | @Test func scenario_expected() ^[C4 medium: 4 examples] | fun scenario_expectedBehavior() ^[C4 high] |
| Identifier | userProfile.continueButton ^[C5 high] | userProfileContinueButton ^[C5 medium] |
| Localization key | UserProfile.ContinueButton ^[C6 high] | user_profile_continue_button ^[C6 high] |
| DI | UserProfileDependencyConfigurator ^[C7 high] | UserProfileModule (Hilt) ^[C7 fallback: defaults] |

Confidence summary:
  iOS:     7/7 high, 0 medium, 0 low, 0 fallback
  Android: 5/7 high, 1 medium, 0 low, 1 fallback
```

AskUserQuestion shape:

```
header: "Conventions"
question: <localized: "Pass B will render with the above conventions. Approve, override, or cancel?">
options:
  - label: "Approve, render"
    description: "Proceed to Phase 2b with these conventions"
  - label: "Override individual cells"
    description: "Open follow-up questions for cells you want to change"
  - label: "Cancel"
```

On `Approve`: proceed to Phase 2b directly.
On `Override`: for each cell the user wants to change, surface one follow-up AskUserQuestion with the current value and an `Other` for the new value. Persist overrides into `state.analysisSpec.conventionOverrides[<repo>][<field>] = <new pattern>`. Pass B reads overrides before defaults.
On `Cancel`: halt and write `state.analysisSpec.phase = "cancelled_at_pass_b_preview"`. Drafts in `/tmp/` are kept for inspection.

Empty submit (`feedback_no-inferred-defaults-from-empty-answer`): re-ask the same question. Do not infer consent.

Skip condition: Phase 2a is skipped only when every `evidence.conventions[<repo>].*.confidence` is `high` AND `state.analysisSpec.conventionRisks[]` is empty AND the user did not pass `--preview-conventions` flag. Otherwise it always runs.

#### Phase 2b - Per-platform render (Pass B)

For each `platform` in `state.analysisSpec.platforms[]`:

1. **Resolve standards binding source** (one canonical source per platform):
   - `ios` → `prefs.projects[<key>].standardsFile` if present → glob `~/<project>-iOS-Standards.md` → `~/.claude/rules/swiftui-qa.md`
   - `android` → `~/.claude/rules/kotlin-android.md` first → `evidence.standards[]` entries whose path contains `android` or `kotlin`
   - `backend` → `evidence.standards[]` entries matching language hints (`python`, `go`, `node`, `fastapi`) → fall back to `~/.claude/rules/security.md` + `code-style.md`
   - `frontend` → `evidence.standards[]` entries matching `react`, `vue`, `next`, `sveltekit` → `~/.claude/rules/code-style.md`
2. **Apply per-platform omission rules.** Backend-only file drops Sections 5, 6, 7, 8, 16. Frontend with no UI inventory still keeps 5 (UI exists in code). Sections 1, 2, 4, 9, 13, 14, 20, 21 always present per Locked decision 2 + 13.
3. **Resolve mode.** If user passed `--lite` → Lite. If user passed `--full` → Full. Otherwise use `state.analysisSpec.liteModeAuto`. Lite mode renders only Sections 1, 2, 4, 9, 13, 14, 21 plus optional 23.
4. **Produce YAML front-matter header** (see `$HOME/.claude/multi-agent-refs/analysis-template.md`). Include `profile: <state.analysisSpec.profile | global>` and `platform: <platform | none>` so the validator applies the right contract per profile (Locked 32) and recognises the stack-optional render (Locked 35), `mode: full | lite`, plus `ui_tests: <state.analysisSpec.options.uiTests | false>` and `a11y_depth: <state.analysisSpec.options.a11yDepth | basic>` so the pre-dispatch validator can enforce the opt-in coverage (15.6 present when ui_tests, 16.2 walkthrough present when a11y_depth is full).
5. **Read conventions for this platform's repo.** For each cell Pass B fills in Section 13 and in any per-platform projection (Sections 5, 6, 7, 8, 10, 11, 13, 14, 15, 16, 17), read `state.analysisSpec.evidence.conventions[<repo>].<field>` and emit the value with a footnote (Locked 24). If `conventionOverrides` has an entry for that field, use the override and footnote with `^[user-override: <reason>]` instead of evidence path.
6. **Concatenate non-null sections in canonical order.** Numbering stays sequential `1..N` over the rendered set (omitted sections do not create gaps).
7. **Schema validation** on the per-platform spec object:
   ```bash
   python3 -c "import json,jsonschema; jsonschema.validate(json.load(open('state/<feature>-<platform>.json')), json.load(open('$HOME/.claude/schemas/analysis-spec.schema.json')))"
   ```
   On failure for any platform, surface the error and stop before Phase 3 (do not draft partial outputs).
