# Evidence gathering (Phases 1, 1b, 1b.1, 1b.2, 1c, 1d)

> Evidence gathering for the analysis engine. Loaded on demand by `/multi-agent:analysis` and by pipeline Phase 1. Produces `state.analysisSpec.evidence.*`; nothing here writes a document.

### Phase 1 - Fetch & extract (parallel)

For each entry in `state.analysisSpec.contextLinks[]`, fan out by `type`. Entries tagged `binding: true` (Q5 inputs) land in `evidence.standards[]` regardless of underlying source type so Section 7 can iterate them as one set.

| type | Command | Output bucket |
|------|---------|---------------|
| swagger | `~/.claude/lib/fetch-swagger.sh <url>` | `state.analysisSpec.evidence.swagger[]` |
| confluence | `~/.claude/lib/fetch-confluence.sh <url>` | `state.analysisSpec.evidence.confluence[]` |
| figma | **MUST** establish the Figma access tier per Locked decision #12. Tier 1 (preferred): call `mcp__claude_ai_Figma__get_design_context` (primary), `mcp__claude_ai_Figma__get_screenshot`, `mcp__claude_ai_Figma__get_metadata` for every node ID; auth failure runs `authenticate` + `complete_authentication` then retries. Tier 2 (fallback): `GET https://api.figma.com/v1/files/{fileKey}/nodes?ids={nodeId}` for design context and `GET /v1/images/{fileKey}?ids={nodeId}&format=png&scale=2` for screenshots, with PAT from `~/.claude/lib/credential-store.sh get <logical-key>` (logical key = `prefs.global.keychainMapping.figma`); canonical component resolves via repo `*.figma.swift` / `*.figma.kt` mapping. Tier 3 (last resort): user-attached screenshot, `codeConnectSnippets: []`, forced Open Question. Capture each `CodeConnectSnippet` block verbatim when on Tier 1 (component name + modifier chain). **Annotation capture (when the project `figma-config` has `annotations.enabled`):** on Tier 1, walk the `annotations[]` the `get_design_context` payload returns for each node (read `label` / `labelMarkdown`); on Tier 2, run `~/.claude/lib/fetch-figma-annotations.sh --file-key <fileKey> --node-id <ids> --lang-prefixes <config.annotations.langPrefixes>`. Parse each annotation by the configured language prefixes (prefixed `TR:/EN:` wins, else line1/line2, else single) and persist to `annotations[]`. The annotation is the authoritative copy for its node (Locked 3); the visible text layer is a placeholder. Persist `state.figmaAccess.tier`. See Locked decision #12 and `$HOME/.claude/rules/figma-pipeline.md` "MUST: Figma access - 3-tier fallback chain". | `state.analysisSpec.evidence.figma[]` (records: `nodeId`, `screenshotUrl`, `codeConnectSnippets[]`, `tokens[]`, `textLayers[]`, `annotations[]`, `tier`; see `analysis-spec.schema.json`) |
| jira | `gh api` or Jira REST API for issue summary | `state.analysisSpec.evidence.jira[]` |
| document | `~/.claude/lib/fetch-document.sh <path-or-url>` (`.docx` via python3 stdlib, `.md` / `.txt` read directly, `.pdf` only when `pdftotext` exists). Exit `6` is a soft skip, not a failure: record the file with `fetched: false` so Section 21 still cites it as referenced-but-not-fetched. | `state.analysisSpec.evidence.documents[]`, or `evidence.standards[]` when `binding: true` |
| generic-doc | WebFetch on demand | `state.analysisSpec.evidence.confluence[]` (generic bucket) unless `binding: true` -> `evidence.standards[]` |
| `local-file` | `Read` (no fetch) on tilde-expanded absolute path | `state.analysisSpec.evidence.standards[]` |
| `wiki` | `git clone --depth 1 https://github.com/<owner>/<repo>.wiki.git /tmp/<repo>-wiki`, then `Read /tmp/<repo>-wiki/<PageName>.md` (`-` decoded back to space in PageName). Fallback chain: `gh api repos/<owner>/<repo>/contents/<file>.md` for repo-hosted docs if wiki repo is 404. | `state.analysisSpec.evidence.standards[]` |
| `standards-confluence` | `~/.claude/lib/fetch-confluence.sh <url>` (same fetcher as `confluence`) | `state.analysisSpec.evidence.standards[]` |
| `firebase-events:names` | Scaffold rows directly from `metadata.names[]`; one row per name with empty params (filled later by repo grep cross-check) | `state.analysisSpec.evidence.firebase[]` |
| `firebase-events:schema` | `Read` the JSON path; parse `events[]` with `python3 -c json.load`; each entry yields `{name, params: [{name, type, required}]}` | `state.analysisSpec.evidence.firebase[]` |
| `firebase-events:console` | Reference-only; print INFO warning `INFO: Firebase Console URL captured for reference; provide JSON schema or event-name list for structured ingestion.`; no fetch attempt | `state.analysisSpec.evidence.firebase[]` (URL stored as reference link only) |

**Auth / access failure handling** (applies to `confluence`, `standards-confluence`, `generic-doc` when WebFetch returns a login page, 401, 403, or HTML containing `<form action="/login.action"`):

1. Do **not** silently drop the entry. Record `state.analysisSpec.evidence.fetchErrors[]` with `{url, type, reason: "auth_required"}`.
2. Surface a one-line warning to the user during Phase 1 progress output: `WARN: Confluence requires browser auth for <url>. Falling back to repo evidence; add the page text manually to /tmp/analysis-paste.md and re-run if needed.`
3. Continue Phase 1 with remaining evidence sources rather than aborting.

For each repo in `state.analysisSpec.repos[]`, run parallel greps (lightweight surface scan; the deeper 13-bucket extraction happens in Phase 1b):

```bash
# Localization
grep -rEl '(Localizable\.strings|strings\.xml|i18n|locale)' "$REPO_PATH"

# Deeplink handlers
grep -rEl '(UniversalLink|DeeplinkRouter|getDeeplink|onNewIntent|intent-filter)' "$REPO_PATH"

# Push handlers
grep -rEl '(UNUserNotificationCenter|didReceiveRemoteNotification|FirebaseMessaging|FCM)' "$REPO_PATH"

# Firebase Analytics call sites (Phase 1 auto-detect + Phase 1b cross-check)
grep -rEln '(Analytics\.logEvent|firebaseAnalytics\.logEvent|logEvent\(analytics,)' "$REPO_PATH"

# Code Connect
find "$REPO_PATH" -name '*.figma.swift' -o -name '*.figma.kt'
```

Results go to `state.analysisSpec.evidence.repo[]` (presence flags only).

**Confluence-embedded API table detection**: if a Confluence page body contains `Request Path` / `Service Name` / `Response Body` table columns, set `embeddedApiTable=true` and parse the endpoints from those columns (a project may supply its own parser for this). Do not also trigger a separate Swagger fetch.

### Phase 1b - Repo-evidence collector (parallel, per repo)

After Phase 1 finishes, run a deeper scan per repo to enable Section 7's reuse-first rule (Locked decision 11). Output: `state.analysisSpec.evidence.repoEvidence[<repo>]`.

**Whitelist roots per platform** (subset of repo paths scanned to keep the catalogue bounded):

| Platform | Roots |
|---|---|
| iOS | `Domains/`, `Common/`, `Core/`, `App/` |
| Android | `app/src/`, `feature/`, `core/`, `common/` |
| Backend | `src/`, `api/`, `services/` |
| Frontend | `src/`, `app/`, `components/`, `features/`, `lib/` (override with `prefs.projects[<key>].frontendRoots[]`) |

Skip dirs (always): `.build`, `DerivedData`, `Pods`, `node_modules`, `.next`, `build/`, `.gradle`, `vendor/`.

**Candidate set**: build a feature-name slug bundle:

```bash
slugs=$(echo "$featureName" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
# e.g. UserProfile -> "user profile userprofile"
grep -rilE "$slugs" $WHITELIST --include="*.swift" --include="*.kt" --include="*.ts" --include="*.tsx" --include="*.py" --include="*.go" | head -200 > $CANDIDATES
```

If `wc -l < $CANDIDATES` exceeds 200, record `fetchWarnings += "feature-name too generic; candidate cap hit"` in `repoEvidence[<repo>]`.

**13 buckets** (each scanned in parallel with a 30 s timeout per bucket; timeout marks the bucket as `partial`, never blocks the others):

| Bucket | Detection (iOS Swift sketch) | Cross-platform variant |
|---|---|---|
| services | `class .*Service\b`, `protocol .*Service\b`, generated `InfoEndpoint<...>` | `class *Service` / `interface *Service` / `def *_service` |
| dtos | `struct .*(Request|Response|DTO)\b` | `data class *Dto` / `interface *DTO` / Pydantic `BaseModel` |
| useCases | `protocol .*UseCase\b`, `class .*UseCase\b` | `class *UseCase` / `def *_use_case` |
| validationRules | `*Rule.swift`, `*Validator.swift`, Validation/ dir | `*Validator.kt` / `*.ts` zod schemas / pydantic validators |
| domainEntities | `Domain/Entities/*.swift` | `domain/entities/*.kt` / `entities/*.py` |
| routes | Public `enum *Route` matching `DomainRoute` | `*Route` sealed / NavRoute / `routes.*` modules |
| coordinators | `*Coordinator.swift` + `CoordinatorProtocol`/`DomainRouter` | Navigator / NavController helpers |
| diConfigurators | `*DependencyConfigurator.swift`, `Module.kt` (Hilt) | DI container registrations |
| uiComponents | `Common/UIComponents/.../Components/**/*.swift` triplet (Configuration + View + +Modifiers) | Compose @Composable functions / React components |
| tokens | `*Token` enums under `UIAssetTokens/Generated/` | `Theme.kt` / `tokens.ts` / `tailwind.config.*` |
| localizationKeys | `LocalizationStringKeys.swift` public enum + cases | `strings.xml` keys / `i18n/*.json` keys |
| testingIdentifiers | `ui-testing-identifiers.json` | `testTag` strings / `data-testid` constants |
| analyticsEvents | Generated `AnalyticsEvents/*.swift` + `Analytics.logEvent\(` call sites | Equivalent Android / Frontend / Backend telemetry |

**Tagging rule** for each row:

| Tag | Match condition |
|---|---|
| `direct-match` | The item name contains a feature-name slug fragment |
| `same-domain` | Item lives under `Domains/<feature>/` or `feature/<feature>/` |
| `cross-cutting` | Item lives under `Common/` / `core/` / shared roots (not under any single feature) |

**Output JSON shape** (per repo):

```json
{
  "buckets": {
    "services": [
      {"name": "UserProfileService", "file": "Domains/UserProfile/Services/UserProfileService.swift", "line": 12, "tag": "direct-match"},
      {"name": "LoggingService", "file": "Common/Logging/LoggingService.swift", "line": 8, "tag": "cross-cutting"}
    ]
  },
  "fetchWarnings": []
}
```

### Phase 1b.1 - Code Connect index (after Phase 1b)

figma-to-swiftui (and the Android equivalent) already produced the feature's components and bound them to Figma via Code Connect. The analysis treats those bindings as the ground truth for "what already exists"  -  it does not re-derive components from the design. Build an index from the `*.figma.swift` / `*.figma.kt` files found in Phase 1:

```bash
# Code Connect figma(...) calls carry the Figma URL (fileKey + node-id)
grep -rEn 'figma\("https://www\.figma\.com/[^"]+"' "$REPO_PATH" \
  --include='*.figma.swift' --include='*.figma.kt'
```

For each binding, parse `{ fileKey, nodeId, component, path }` where `component` is the registered component type and `path` is the binding file. Decode the URL `node-id` form (`-` to `:`). Output: `state.analysisSpec.evidence.codeConnect[]`.

Phase 2 fills the design->code link deterministically from this index:
- Section 5.1 `Code Connect` column: the matched component name for that frame's `nodeId` (blank when the frame has no existing binding).
- Section 6 `Mevcut / Existing`: `yes` + `reuse <component> at <path>` when the design `nodeId` (or its `fileKey`) matches an index entry; `no` + `new component` (added to Section 14) when it does not.

When no Code Connect file exists in any selected repo, the index is empty and Section 6 falls back to the Phase 1b `uiComponents` heuristic (the prior behavior).

### Phase 1b.2 - Main component variant matrix (after the Code Connect index)

The index answers "does this component exist and is it bound". It does not answer "what can it do". For every bound component, walk from the instance to its definition and read the whole axis:

1. Resolve the instance's `componentId` to its main component / component set.
2. Read the full variant axis. Tier 1: `mcp__claude_ai_Figma__get_metadata` on the component set. Tier 2: `GET /v1/files/{fileKey}/nodes?ids=<componentSetId>` and read `componentPropertyDefinitions` (each axis plus every one of its values); `GET /v1/files/{fileKey}/components` when the set id is not known.
3. Diff the full axis against the values this screen actually uses.

Output: `state.analysisSpec.evidence.variantMatrix[<component>] = { axis, allValues[], usedValues[] }`, which fills Section 6.X.

Why it lives here and nowhere later: Locked 30 forbids Figma access from Phase 2 onward, so an axis not captured now cannot be recovered - Section 13.6 Preview and Section 15.2 Snapshot would then be validated against a subset nobody could check.

A component with no variant axis (a plain component, not a set) records `axis: null` and is not a finding.

### Phase 1c - Convention extraction (parallel, per repo)

Per Locked decision 23. After Phase 1b finishes, run `~/.claude/lib/extract-conventions.sh <repo-path> <platform>` for each selected repo. The script returns a JSON document with seven pattern groups:

| Group | Keys |
|---|---|
| C1 folder structure | `folderStructure` |
| C2 class naming | `stateHolderNaming`, `viewNaming`, `navigatorNaming`, `useCaseNaming`, `repositoryNaming`, `dtoNaming` |
| C3 UI state model | `uiStateModel` |
| C4 test method naming | `testMethodNaming` |
| C5 accessibility identifier | `accessibilityIdentifier` |
| C6 localization key | `localizationKey` |
| C7 DI registration | `diRegistration` |

Each field has shape `{ pattern, example, confidence, evidenceFiles, alternativeCandidates }` where `confidence` is one of `high` (5+ examples, dominant), `medium` (3-4 examples, majority), `low` (2 examples or mixed), `none` (no evidence).

**Output**: `state.analysisSpec.evidence.conventions[<repo>]`.

**Risk auto-population (Locked 23)**: when any field returns `confidence: "low"` or `"none"`, push a row onto `state.analysisSpec.evidence.conventionRisks[]`:

```json
{
  "repo": "<repo>",
  "field": "<pattern key>",
  "confidence": "<low | none>",
  "fallback": "conventions-defaults.md:C<n>-<platform>",
  "question": "<convention-key> evidence is insufficient. Apply default <pattern> or override?"
}
```

Phase 2 Section 20 Risks reads this list and emits one open question per entry.

**Fallback source**: when `confidence == "none"` AND `evidence.standards[]` does not contain an explicit rule for that field, the renderer reads `$HOME/.claude/multi-agent-refs/conventions-defaults.md` and applies the platform default.

**Caching (Locked 27)**: compute `evidence_digest = sha256(featureName || sorted(platforms) || hash(evidence.repoEvidence) || hash(evidence.conventions))`. Cache key on disk: `/tmp/multi-agent-analysis-cache/<digest>.json` with mtime <= 24h. Cache hit skips Phase 1b and Phase 1c. `--no-cache` flag forces re-run.

Phase 1d is deliberately absent from the digest inputs. Community signal changes by the hour, so folding it in would produce a new digest on every run, invalidate the cache every time, and re-run the two expensive repo phases the cache exists to skip. The consequence is worth stating plainly: a cache hit reuses yesterday's signal rows. That is the correct trade for an advisory tier, and `--no-cache` is the way to refresh them.

### Phase 1d - Outside facts (optional, analyst toolkit)

Runs after Phase 1c, only when `ai-analyst-toolkit` is enabled. Not enabled is a recorded no-op, never a halt: this phase adds context the repo cannot supply, and a run without it is smaller, not wrong.

Two tiers, and the difference decides where a fact is allowed to appear.

| Tier | Sources | Enabled by | Bucket | May appear in |
|---|---|---|---|---|
| Evidence | `evidence-github`, `evidence-registry` | `prefs.global.analyst.evidence[]` (default `["github","registry"]`) | `state.analysisSpec.evidence.outside[]` | any section, cited `GitHub:<owner>/<repo>#<n>` or `Release:<pkg>@<ver>` (Locked 3) |
| Signal | `signal-community` | `prefs.global.analyst.signals[]` (default `["stackoverflow","hackernews"]`), plus `prefs.global.analyst.webSignals` for Reddit / X | `state.analysisSpec.evidence.signals[]` | Section 20 Risks only, as a "reported in the wild" row with its link and date |

Read those three prefs before dispatching: a source absent from its array is not queried and is not reported as unreachable either, because the user turned it off rather than the network failing. `prefs.global.analyst.evidence[]` set to `[]` disables the whole evidence tier without disabling the plugin.

What to ask for: whether a dependency the feature touches has an open upstream bug, what a pinned version actually changed, whether prior art exists for the pattern being introduced, and whether anyone outside this team has reported the same symptom.

The tier boundary is not advisory. A signal row that reaches Section 4 (business rules), Section 9 (API contracts) or Section 13 (architecture) is a Locked 3 violation: those sections carry claims a reviewer can check, and a forum post is one person's experience, not a fact about the system being specified.

Per source: reachable and answered, reachable and empty, or unreachable. All three are recorded; only the third produces a `fetchErrors[]` entry, and none of them halts.

**Lite mode auto-detection (Locked 25, v9.1.0 scoring)**: at the end of Phase 1c, evaluate three signals and score each:

| Signal | True condition | Score |
|---|---|---|
| Confluence spec body lines | < 100 | 1 |
| Figma frames count | <= 1 | 1 |
| Repo evidence direct-match count | >= 8 | 1 |

`liteModeAuto = (totalScore >= 2)`. Two of three signals true is enough; the v8.12.0..v9.0.x AND-threshold (all three) forced too many small features into Full mode when one signal was marginal (e.g. a tiny spec with 2 Figma frames). User overrides via `--full` or `--lite` always win over scoring.
