# Discovery Reference — Scope Gate · Access Vector · Dimension Catalog

The reference knowledge for **Discovery** (`discover` harness, Mode 1). It carries all three of Discovery's steps in the order they run (preceded by corpus-root resolution, Step 00 below):

- **Part A — Scope-feasibility gate** (Step 0): resolve the target's identity and emit a verdict so the run never wastes effort on an infeasible target.
- **Part B — Access vector** (Step 1): grade the four-axis capability vector that decides which dimensions are reachable and which method each uses.
- **Part C — Dimension catalog** (Step 2): the canonical list of dimensions, their detection signals, collection strategy, and owning agent.

`ingestion` also reads **Part C** to dispatch the right agent with the right method.

---

# Step 00 — Resolve the corpus root (before the gate)

Before Part A, decide **where this run writes** — the harness picks intelligently, it does not hardcode `research/`:

> **EXCEPTION — a calibration run defers this step.** If this run is a calibration (`calibration.md`), do
> **not** restore or read the prior corpus yet: re-derive the sampled claims from the live product and write
> your observations down first, then diff. Reading the prior claim first anchors the re-derivation and turns
> the exercise into a consistency check. Everything below applies **after** step 3 of the blind protocol.

0. **Check git history BEFORE declaring any target fresh.** The filesystem is not the whole record — run `git log --oneline -- <corpus>/<target>`, `git ls-tree -r --name-only HEAD -- <corpus>/<target>`, and `git stash list`. **A prior corpus may exist in HEAD (or a stash) with no working-tree copy.** If one is found, restore it (`git checkout HEAD -- …` / `git stash pop`) and **RECONCILE against it** — a prior run is an *independent corroboration lane* and may refute the current one. The user's own wording is a signal too: "**re**run", "again", "update" all mean a prior run is expected. (Origin: Emergent — a run checked `ls research/` only, found the folder thin, and spent its opening declaring a re-run to be a first run; 110 files of prior corpus were sitting in HEAD.)
1. **Reuse an existing discovery corpus** if the project has one — search for any folder containing a prior run's `*/00-scope-verdict.md` (commonly `research/`, but also `discovery/`, `analysis/`, or a folder the user names).
2. If several exist, prefer the one the user points at, else the most recently written.
3. If none exists and the user hasn't named one, default to `research/` at the repo root.
4. **Confirm the chosen root in one line** before writing `00-scope-verdict.md`.

All `research/<target>/` paths in the harness are the _default form_, relative to the resolved `<corpus>/`.

---

# Part A — Scope-feasibility gate (Step 0)

Run this **before** any dimension probing. It resolves the target's identity and emits one of four verdicts. The rules are mechanical — execute them with cheap probes (`WebSearch`, `curl`, `git ls-remote`, `npm view`) in well under a minute.

Output: `research/<target>/00-scope-verdict.md` + a `scope:` block in `00-recon-plan.md`. If the verdict is not `accept`, Discovery **halts** (presents the narrowing prompt, or refuses) and does not proceed to dimension probing.

## Verdicts

| Verdict | Meaning | Downstream |
| --- | --- | --- |
| **accept** | bounded target with ≥1 real surface | continue to dimension probing |
| **narrow** | too-broad-suite or ambiguous-name | present the narrowing prompt; halt for user reply |
| **redirect** | a commodity primitive (behavior = its public spec) | offer a facet menu; halt for user reply |
| **refuse** | ethically/legally out-of-bounds (hard stop only here) | stop; no run |

A **no-reachable-surface** result is a **soft prompt** ("log in / give me a URL or repo"), not a hard refuse — only the ethics gate (Gate B) is a hard refuse.

## The decision algorithm (priority-ordered; first match wins)

**0a — Resolve identity.** If the user gave a concrete URL/repo/package, identity is fixed → skip ambiguity. Else `WebSearch "<target>"` + `"<target> software"`; collect canonical homepage, vendor, one-line category.

**Gate A — ambiguity.** Top results map to ≥2 unrelated vendors/categories → `narrow` (disambiguate). STOP.

**Gate B — ethics.** Resolved product is piracy/cracking/malware/CSAM/credential-harvesting/spyware, or the only way in is bypassing auth/paywall/TOS the user has no right to → `refuse` (ethics). STOP. _(Highest priority once identity is known — never proceed on an out-of-bounds target even if reachable.)_

**Gate C — suite size.** Fetch homepage; count distinct independently-marketed products N (nav "Products" items each with their own `/pricing` or `/docs` tree; or a `WebSearch "<target> products"` grid). If N ≥ 4 **OR** the entity is on the umbrella seed list → `narrow` (pick-product). STOP.

> Umbrella seed list: AWS, Azure, GCP, Salesforce, Microsoft 365, SAP, Oracle Cloud, Google Workspace, Adobe Creative Cloud, Atlassian. Guard against false positives: "one platform, many _modules_ sharing one account/API/pricing page" (Stripe: Payments/Billing/Connect) is **one product → accept**, not a suite. The N≥4 count alone isn't enough — require separate pricing/docs trees, not just a broad nav.

**Gate D — primitive.** Target is a single foundational building block whose behavior is a published spec with no hidden app surface (managed primitives like S3/EC2/Lambda; single-purpose util libs with <~5 exports; protocol/spec names; a raw DB engine) → `redirect` (pick-facet). STOP.

> Bias: only `redirect` when behavior is fully a public spec. When a hosted SaaS wrapper exists (Redis Cloud, MongoDB Atlas), prefer `accept` on the wrapper.

**Gate E — reachability.** Run the cheapest probe per dimension as a boolean (does ANY surface exist?): `git ls-remote` OR npm/PyPI hit OR `llms.txt`/docs subdomain OR homepage-200-with-content OR user-has-session. If ZERO surfaces → **soft prompt** ("No reachable surface — log in, or give me a repo/docs/URL"). If exactly one thin non-session surface → `accept` (thin) with a shallow-coverage warning.

**Fallthrough → `accept`.** Compute the access vector (Part B) and proceed to dimension probing.

**Recursion:** a narrowed pick re-enters Step 0 (so "Salesforce → Marketing Cloud" is itself feasibility-checked and may narrow again). **Max narrow-depth = 2**, then accept-the-narrowest with a partial-coverage warning.

## `00-scope-verdict.md` shape

```markdown
---
target: <slug>
scope_checked_at: <YYYY-MM-DD>
verdict: accept | narrow | redirect | refuse
verdict_reason: clean | too-broad-suite | primitive | ambiguous-name | no-surface | ethics | over-scoped
resolved_identity: <canonical homepage / repo / package>
suite_product_count: <N>
surfaces_present: [codebase?, docs?, api?, website?, session?]
awaiting_user: <true|false>
---

# <Target> — Scope Verdict: <VERDICT>

## Resolution (what the name resolved to + evidence)

## Gate trace (which gate fired A–E + the verbatim signal)

## Verdict & reason

## Narrowing prompt (if narrow/redirect — the exact question + options)

## If refused (one-line reason + what would make it acceptable)
```

Mirror `verdict` into a `scope:` frontmatter block on `00-recon-plan.md` so ingestion reads one field.

## Narrowing prompt templates

**NARROW-SUITE (Gate C):** "I can recon a focused product, but **<Target>** is a suite of ~<N> independently-built products — a single run would be shallow across all of them. Which one should I tear down?" + a numbered list auto-built from the homepage product nav, each tagged with its likely access-grade. Always append "0. A specific feature within one of these (tell me which)."

**NARROW-DISAMBIGUATE (Gate A):** "**<Target>** resolves to more than one product. Which did you mean?" + numbered candidates (vendor · one-line · URL) + "none of these — give me a URL."

**REDIRECT-PRIMITIVE (Gate D):** "**<Target>** is a documented primitive, not a product with hidden internals — its behavior is the public spec. I can still recon a facet. Which angle?" + a facet menu (for a managed service: console/dashboard UX · public API surface · consistency/semantics model · pricing & limits as a product · competitive positioning) + "Or point me at a real product that USES <Target> and I'll recon that instead."

## Worked verdicts (regression fixtures)

| Target | Gate | Verdict | Note |
| --- | --- | --- | --- |
| AWS S3 | D | redirect (primitive) | "behavior is the public API; pick a facet or a product built on S3" |
| Salesforce | C | narrow (suite) | "pick a cloud: Sales / Service / Marketing / Flow Builder / Apex / …" |
| Microsoft 365 | C | narrow (suite) | umbrella seed list; options Word/Excel/Teams/… each tagged |
| Notion | fallthrough | accept | single product; docs+api+website+session, no public monorepo |
| Liveblocks | fallthrough | accept (Case 1) | open monorepo + SDKs + docs + OpenAPI + site + session |
| bare "Atlas" | A | narrow (ambiguous) | MongoDB Atlas vs Ory Atlas vs Palantir |
| leftpad-class util | D | redirect (primitive) | "<5 exports; source IS the spec" |
| coming-soon splash, no session | E | soft prompt | "no reachable surface — log in or give me a repo/URL" |
| torrent/credential-stuffing service | B | refuse (ethics) | no prompt, no run |

---

# Part B — Access is a capability VECTOR (Step 1)

A target's "access" is **four orthogonal axes**, each carrying `{value, confidence, evidence}`. The vector — not a single grade — decides which dimensions are reachable and which extraction _method_ each uses.

| Axis | Values | Unlocks dimensions | Anchors |
| --- | --- | --- | --- |
| **source** | full · partial · none | codebase, packages | _Case 1_ (full = OSS repo) |
| **runtime** | reachable · gated · none | deployed-client-bundle, api, infra-backend-fingerprint, wire-capture | _Case 2_ (reachable + source:none) |
| **auth** | have-login · none | session | _Case 3_ (have-login) |
| **presence** | rich · thin · none | docs, website, community | (almost always rich) |

The three named **cases** are presets over this vector — a target can light several at once (Liveblocks = source:full + runtime:reachable + auth:obtainable + presence:rich → nearly all dimensions). Rules:

- **`auth:obtainable` is a hint, not actionable** — the harness never self-signs-up; it only counts as `have-login` if the _user_ hands over a session.
- **A _claimed_ login is `auth:claimed` (confidence medium), not `auth:have-login` (high), until a cheap live probe confirms it.** A user saying "I'm logged in" — or a session being merely _expected_ — is not proof: profiles expire, a fresh tab may not restore an in-memory token, and a deep-link can bounce to a login wall while the root looks authed. Grade `auth:claimed` until Discovery (or the `session` agent's first step) navigates the app and confirms an **authenticated surface** (a dashboard, a populated `viewer`/`/me`, a token in the store) — only then promote to `have-login`. Grading a claimed login `high` lets a session collapse to `blocked` after dispatch, with the wasted dispatch hidden behind a confident grade.
- **Dimension-level access overrides the target headline** for method selection (a Case-1 target can still have a Case-3 session dimension).
- Method swaps on axis _value_: `source:full` ⇒ `clone-and-map`; `source:partial` ⇒ `source-map-reassembly`; `runtime:reachable + source:none` ⇒ `bundle-string-mine` + wire-capture.
- **`source:full` also unlocks a _runtime you own_ — prefer booting it locally over deferring the gated runtime lanes.** An OSS repo is not only `clone-and-map` read-surface: stand up the project's own **local dev / self-host** (`docker-compose` / dev quickstart) for a first-party `localhost` runtime, then run `session` / `wire-capture` / Cartography against it **without** the vendor's cloud login or any TOS/credential gate. **Code access via the public repo is the cornerstone of strategy** — for any open-source target, self-host is the preferred path to the runtime/experiential lanes (it converts an otherwise-gated experiential axis into a fully-authorized capture). Confirm before anything that spends or mutates; the local boot itself is you-own-it / read-only. (Origin: Twenty — `source:full` was read but the app was never booted, so the runtime lanes were deferred when a local `docker compose up` would have unlocked them.)

Discovery records the resolved vector in the `access_grade` frontmatter of `00-recon-plan.md`.

> **A wide CSP / allow-list is a hypothesis GENERATOR, never evidence of use.** A `content-security-policy`
> (especially `connect-src` / `script-src` / `form-action`), a `permissions-policy`, or a bundled vendor SDK
> tells you what the app **may** contact — not what it does, and never what **role** that vendor plays.
> Record the **declared origin as fact** and the **role as hypothesis**, tagged `verify:`. A vendor's mere
> presence in a bundle or allow-list is not proof it is on the critical path. (Origin: Emergent — one CSP
> header yielded ~25 vendor leads in a single request, an outstanding lead generator; the `verify:` tags are
> what stopped three collectors echoing those leads as established fact.)
>
> **But the INVERSE error is just as costly — and it is the one that actually bit.** A `verify:` tag
> resolves to **`confirmed`** or **`not-yet-observed`** — **NEVER to `refuted`** on the strength of an
> *absence* in an instrument you have not shown is capable of seeing the thing. (Origin: Emergent — the run
> hypothesised from the CSP that **Pusher carried streaming**, failed to observe a single Pusher frame, and
> published "Pusher does not carry build streaming" as a refuted hypothesis and a methodology win. **Pusher
> was live the whole time** — `state: connected`, `cluster: us2`, channel `user-{uuid}`, carrying
> `traj-update`/`terminal-state`/`streak-update` pings. The `window.WebSocket` wrapper had been installed
> *after* app boot and was **structurally blind** to the socket the Pusher library opens at boot
> (`tradecraft.md` gotcha #13/§2.12), whose prescribed remedy — the `bind_global` library tap — was never
> installed, even though `pusherTransportTLS` sat in the storage census as a live signal. A CSP lead that you
> then fail to observe is **unconfirmed, not disproved**.)

> **Never infer target geography from `cf-ray` / edge-PoP codes.** Anycast CDNs route to the PoP nearest the
> **capture host**, so a `BOM`/`BLR`/`FRA` suffix describes *the observer*, not the target. Geography needs a
> real lane: the legal entity on the site or in whois, job-post locations, billing currency, or a
> data-residency statement. (Origin: Emergent — BOM/BLR PoPs were read as evidence of an India-based
> operation; the conclusion happened to be right, but on **other** evidence — an Indian legal entity, 28/43
> engineering roles in Bangalore, and INR billing — while the PoP reasoning was simply invalid.)

> **Hypotheses are not facts (seed them tagged, never echo them).** Any _derived inference_ Discovery makes beyond the four graded axes — e.g. "this `localStorage` key implies a GraphQL backend", "this host is the realtime endpoint" — is recorded in `00-recon-plan.md` **as a labelled hypothesis**, never as a plan fact, and is tagged `verify:may-be-stale` when seeded to a collector (CLAUDE.md Mode 2) so Ingestion **re-derives** rather than echoes it. A wrong inference seeded as framing propagates: a real run's `nustackAnonId ⇒ GraphQL` guess was repeated (hedged) by two captures before the `session` dimension refuted it (the backend was RSC + Server Actions; the key was an NuStack.io tracker).

---

# Part C — The dimension catalog (Step 2)

| # | Dimension | Agent | Unlocking axis | One-line strategy |
| --- | --- | --- | --- | --- |
| 1 | **codebase** | `codebase` | source:full | clone → structural map → per-package fan-out |
| 2 | **docs** | `docs` | presence | `llms.txt` → else crawl/clip → organize by section |
| 3 | **packages** | `packages` | source/presence | registry metadata + exports + type defs |
| 4 | **api** | `api` | runtime/presence | OpenAPI/GraphQL → endpoint catalog (+ **MCP/tool-catalog fold-in**) |
| 5 | **website** | `website` | presence | crawl public pages → features, pricing, positioning |
| 6 | **community** | `community` | presence | first-party changelog/issues/discord themes |
| 7 | **session** | `session` | auth:have-login | delegate to the session playbook (§9 of `ingestion`: 3-pass, P0–P6) |
| 8 | **deployed-client-bundle** | `deployed-client-bundle` | runtime:reachable | unauth SPA: route table, API-path catalog, flags, env, source maps |
| 9 | **infra-backend-fingerprint** | `infra-backend-fingerprint` | runtime/presence | DNS, cert-transparency, CDN/cloud, headers (+ **sub-processor/trust fold-in**) |
| 10 | **wire-capture** | `wire-capture` | runtime (source:none) | the 3-rung ladder for non-browser/no-source clients (gated) |
| 11 | **distribution-artifacts** | `distribution-artifacts` | runtime (non-web client) | extension CRX / Electron `.asar` / mobile binary / CLI |

> **Deferred (available on request, not built):** `external-reputation` (G2/Reddit/HN — the only "what others say" surface) and `hiring-intel` (job posts leak the backend stack). Strong candidates, declined for this iteration. Re-add via self-correction if a run needs them. **Each now carries its own conditional trigger — see below.**
>
> **Trigger 1 — `hiring-intel` on any `source:none` target.** When the access vector grades **`source:none`**,
> flag `hiring-intel` **recommended-for-this-run** in `00-recon-plan.md`. Rationale: every other
> non-marketing lane on a closed target (`deployed-client-bundle`, `session`, `wire-capture`,
> `distribution-artifacts`) ultimately reads **an artifact the company ships to a client** — so they all
> share one blind spot, **any backend component that never reaches a browser**. Job posts are the only
> cheap lane that is independent of both marketing *and* the client. (Origin: Emergent — the corpus asserted
> a "Python/FastAPI backend" from bundle+wire mining; the company's own declared stack names **Go co-equal
> with Python** and `FastAPI` appears **zero times** in 45 postings. A Go service ships no JS and answers in
> plain JSON, so it is invisible to those instruments *by construction*. The same lane also surfaced Kafka,
> PostgreSQL/Redis as first-tier stores, AWS as a second cloud, and an in-house **model post-training**
> function that refuted a pure-inference-broker read.) Grade a posting-derived claim by §3 `ats-api` (high)
> when it comes from the official ATS API, `crawl-clip` (medium) when from a careers page or aggregator.
>
> **Trigger 2 — `external-reputation` on a closed feedback loop.** When `community` returns **no first-party feedback loop** (no public issues/roadmap/changelog/forum — feedback funnelled to a gated channel), flag `external-reputation` as **recommended-for-this-run** in `00-recon-plan.md`, so the user-sentiment blind spot is a surfaced choice rather than a silent gap (the `community` user-pain claims are then capped at tentative — evaluation closed-feedback cap).
>
> **When you DO run `external-reputation`, split the sample by review SOURCE before reporting any average.**
> Review platforms commonly label whether a review was user-initiated or **vendor-solicited** (Trustpilot
> tags `AFSv2` / `FileUploadInvitation` and exposes `hasRecentlyInvitedUsers`). A blended average over a
> vendor-controlled sample is not a satisfaction measure. Report the **organic** figure, and look for a
> **natural control group** — an *unclaimed* or duplicate profile for the same product, where solicitation
> is impossible, is the strongest corroboration available. State the *effect* from the platform's own
> labels and **never allege fake reviews**: soliciting is legitimate, and the finding is about **sampling**,
> not fraud. (Origin: Emergent — published 3.0 decomposed to organic **2.43** (n=141) vs solicited **4.58**
> (n=59); the unclaimed sibling profile sat at **2.2**.)

An **absent** dimension is a finding — record it with its evidence; never silently drop it.

## 1–7 — the core dimensions (strategy in brief)

See git history / the source playbook for the full strategy text; in brief:

- **codebase** — shallow-clone into `source/<target>/`, read orientation files + manifests, map packages + internal dep hierarchy, return a per-package work-list for the main-session fan-out.
- **docs** — prefer in-repo docs, then `llms.txt`/`llms-full.txt`, then crawl; organize by section.
- **packages** — per public package: registry metadata, `exports` map + `.d.ts` type surface, health. **Registry-ownership cheap-test (grade ownership before calling an SDK "partial/unverified").** Read the registry `author`/`maintainers`/`summary`/`project_urls`/`Homepage` fields and check the dependency set for a generator fingerprint (Stainless: `httpx`/`pydantic`/`anyio`/`distro`/`sniffio`
  - in-file "generated by Stainless" headers; Speakeasy; OpenAPI-Generator). An `author`/`summary` that says "official … library for the X API" + a vendor-domain `project_urls` confirms **official** ownership at **high** confidence even when the _source_ repo is private — the published tarball IS the public-API mirror (a high-confidence `registry-metadata` anchor), so it is **not** "partial." Tag a genuinely third-party/community package (different author/scope) as such, but don't down-grade an official-but-private-source SDK to "unverified."
- **api** — fetch the spec verbatim, build an endpoint catalog (auth, envelope, pagination, webhooks). **Grade the method from a RETRIEVED artifact, never a label.** Before grading `openapi-verbatim`, actually fetch the candidate spec URL and confirm a **parseable JSON/YAML body** — a docs page merely _titled_ "OpenAPI" with no served `spec.json` (or a `/swagger` that 403s) is `docs-reconstructed` (capped medium), and Discovery must pre-grade it that way so the plan matches reality. **Seed the warm-entry path that actually returned 200 in the probe — never a sibling that 403'd:** if `/rest/open-api/core` 403s while `/open-api/core` serves 200, the plan must name the **200** alias, else the collector inherits a dead path and burns a step re-deriving the working one mid-run (Twenty: recon seeded the 403 variant; the `api` collector had to re-find the live alias). **A sibling-declared host/op/route count is a coverage checklist (a floor), never a ceiling** — every collector expands from sibling-mined hosts/routes and records the predicted-vs-found delta. **NEW fold-in:** also probe MCP/tool surfaces — `/.well-known/mcp`, an MCP server manifest, a function-calling tool schema, an LLM "actions" file — and write `raw/tool-catalog.md` (an MCP tool list _is_ an API catalog; it lives here, not in its own dimension). **Unauth MCP-probe recipe (when the tool catalog itself is auth-gated):** a `GET /mcp` → `405` body often names the transport (e.g. "stateless; only POST is supported" = streamable-HTTP); `GET /sse` presence/absence tells legacy-SSE vs streamable; and `/.well-known/oauth-authorization-server` + `/.well-known/oauth-protected-resource/{name}` reveal the **OAuth/PKCE model + issuer** (e.g. a `jwks_uri` on the auth vendor names Clerk/Auth0/etc.) — all without a token. Capture transport + auth + issuer from these, reconstruct the tool list from docs/SDK, and mark the verbatim `tools/list` **partial** (OAuth-gated).
- **website** — crawl home/features/pricing/customers/integrations; extract positioning, pricing matrix, target market.
- **community** — _first-party only_: changelog cadence + direction, issue/discussion themes, blog.
- **session** — the deepest, opt-in, user-gated dimension; delegates to the session playbook in `ingestion` §9 (3-pass, P0–P6, captures/raw/curls). For any `auth:have-login`/`auth:claimed` target, Discovery **seeds the Pass-2 decision** (`pass2: offered-pending | declined-by-user | deferred`) into `00-recon-plan.md`, so a Pass-1-only (read-only) session is a _recorded choice_, not a silent omission of the write surface (ingestion §9.2).

## 8 — deployed-client-bundle — `deployed-client-bundle`

**Unlocked by:** `runtime:reachable`. The **unauthenticated app shell** — distinct from `website` (marketing) and `session` (authenticated). Works on closed SaaS with no repo and no login.

**Detection:** an app subdomain (`app.<target>.*`) or SPA route serves a JS bundle; `sourceMappingURL` present; a `/assets/*.js` graph. **Two cheap Discovery probes pre-grade the method and surface a leak early:** (a) does an entry chunk's `sourceMappingURL` resolve to a `.map`-200? → pre-grade `source-map-reassembly` (confidence high — original `src/` recovers). (b) probe a few bootstrap-config paths (`/config`, `/backend/config/*`, `/env.json`, a runtime-config endpoint) — a 200 here is both a method pre-grade and a possible unauth config/secret leak (a finding).

**Strategy:** fetch the main bundle(s); string-mine the **route table**, the **API-path catalog** (→ append to the shared `dimensions/_shared/api-path-catalog.md`, `source: bundle` — ingestion §6), **feature flags** (→ shared `dimensions/_shared/feature-flags.md`), embedded **env config** (`VITE_*`, `NEXT_PUBLIC_*`, public keys — field-names only, redact values), and reassemble **source maps** if shipped (`method: source-map-reassembly`, confidence high). Digest huge bundles, don't paste. This dimension **owns** the app-API bundle mine — other dimensions cite its catalog by path, never re-mine the same bundle (ingestion §6).

**Artifacts:** `_summary.md` + `raw/{route-table.md, env-config.md, bundle-map.md}` + appends to the shared `dimensions/_shared/{api-path-catalog.md, feature-flags.md}`.

## 9 — infra-backend-fingerprint — `infra-backend-fingerprint`

**Unlocked by:** `runtime`/`presence`. Reconstructs the backend you can't see in the repo.

**Detection:** always attemptable for any live domain.

**Strategy:** DNS records; **cert-transparency subdomain enumeration** (crt.sh → staging/internal/ api hosts); CDN/cloud fingerprint (CNAMEs, IP/ASN, response headers); security headers; tech fingerprint (framework/analytics tells). **Sub-processor fold-in:** fetch the `/legal`, `/trust`, `/security`, `/dpa` pages → `raw/sub-processors.md` — the sub-processor list **names the vendor stack** (cloud, payments, AI provider, email, telephony), which is exactly this dimension's job.

**Artifacts:** `_summary.md` + `raw/{dns-and-subdomains.md, cloud-cdn-fingerprint.md, security-headers.md, sub-processors.md}`.

## 10 — wire-capture — `wire-capture` (first-class, GATED)

**Unlocked by:** `runtime` when the interesting client is **non-browser** or there's **no source** (your Case 2). The runtime-evidence complement to `distribution-artifacts` (static): the binary says what hosts it _might_ call; the capture says what it _actually_ calls.

**Detection:** a non-browser client exists (from distribution-artifacts) OR an unauth SPA makes structured XHRs worth a tap, AND the appropriate ethics rung is authorized.

**Strategy:** the **3-rung ladder** (lowest privilege first) — see `ingestion` §7.1:

1. browser network tap (unauth, default) → `wire-tap-browser`;
2. HAR export → `wire-har`;
3. mitmproxy (**gated, opt-in, user-run, first-party traffic only**) → `wire-mitmproxy`;
4. **first-party SDK-harness** (when the product publishes an official SDK + the _user_ can provision keys) — scaffold + instrument a throw-away client you own (`document_start` taps, full wire) → `sdk-harness` (`ingestion` §7.1). The deepest lane for a `runtime:reachable + source:none` product whose SDK is the only public mirror of the runtime; gated like rung 3.

**Fold note (common path):** when there is no separate non-browser client, wire-capture **folds into `session`** (which owns the browser singleton), **never into a curl/WebFetch collector** (a non-browser collector cannot run rung-1's in-page tap, and routing it there yields zero `source: wire` rows — ingestion §7.1). The folded rung-1 _is_ the interceptor `session` installs; its observed paths append to the shared `dimensions/_shared/api-path-catalog.md` as `source: wire`. A folded wire-capture still leaves a one-paragraph `_summary.md` (`status: folded`, naming the fold target) — never a silent drop.

**Artifacts:** `_summary.md` + `captures/wire-narrative.md` + `raw/flows.json (redacted/digested)` + appends to shared `dimensions/_shared/api-path-catalog.md (source: wire)` + `curls.md`. Record the chosen rung + authorization in `_meta`.

## 11 — distribution-artifacts — `distribution-artifacts`

**Unlocked by:** `runtime` (a downloadable non-web client). Often the closest thing to source for a closed product.

**Detection:** a Chrome/Firefox web-store listing; a "Download for Mac/Windows" (Electron/Tauri) button; an App Store / Play Store listing; a published CLI (`npm i -g`, brew, GitHub releases).

**Strategy:** **extension** — decode `manifest.json` (permissions/hosts/content-scripts = a capability map). **Electron** — `npx asar extract app.asar` → map JS modules + the shared route/API/flag catalog from the desktop client (often richer than web). **mobile** — `Info.plist`/ `AndroidManifest` hosts, ATS exceptions, embedded-SDK list from strings (**redact any embedded keys**). **CLI** — help text/subcommands = an API catalog. Read the artifact's EULA first (see `ingestion` §7.2); never silently decompile a restrictively-licensed binary.

**Artifacts:** `_summary.md` + `raw/{extension-manifest.md, electron-asar-map.md, mobile-strings.md, cli-catalog.md}` (whichever apply) + appends to shared `dimensions/_shared/api-path-catalog.md` (source: distribution).

---

## How Discovery uses this reference

Step 0 — run **Part A** (the gate); halt unless `accept`. Step 1 — grade the **Part B** vector with evidence. Step 2 — for each dimension in **Part C**, check its unlocking axis against the resolved vector, run the cheap detection probe, and record **available / partial / absent** with evidence into `00-recon-plan.md`. Gated dimensions (session, wire-capture rung 3) are flagged for user confirmation before ingestion dispatches them.
