# ADR-135 Playwright Is the Default Tool for Reading Embedded/Hosted Documents (eBooks, Document Posts, Viewers) — Capture the Document Area Only

## Status

Accepted, 2026-08-09.

## Status History

```yaml
status_history:
  - date: 2026-08-09
    status: Accepted
    changed_by: hkl
    reason: |
      Raised while sourcing a 156-page eBook ("How to Build an ERP with Claude —
      A Nexus Story", 2nd Ed.) published as a LinkedIn document post, needed as a
      required input to the 360LM WebERP plan. The session first reached for
      Claude-in-Chrome (CiC) and began paging the viewer by screenshot, which was
      slow, token-heavy and produced no reusable asset. hkl directed a switch to
      Playwright, noting it is "more controlled if not token efficient", and that
      capture should be scoped to the book area only rather than the full page.
      Codified as a standing rule because reading a hosted document is a
      recurring class of work distinct from ADR-124's PWA visual capture.
    changed_via: authored directly per hkl's explicit instruction
```

## Context

**ADR-124** established Playwright-first for *PWA screenshot/screencast automation*, with CiC
requiring explicit per-task approval. It did not cover a different and increasingly common task:
**reading a document that is hosted inside someone else's web viewer** — an eBook or whitepaper
published as a LinkedIn document post, an embedded PDF viewer, a slide deck behind a login.

The 2026-08-09 session exposed why this needs its own rule. Working through CiC, the session:

1. concluded (wrongly) that the publisher had **disabled download**, because no download control
   appeared in the page DOM;
2. began reading 156 pages by **screenshotting the whole viewport**, which is expensive and yields
   images cluttered with LinkedIn chrome rather than clean pages;
3. could not inspect the document at all from page context, because it renders inside a
   **cross-origin iframe** (`media.licdn.com/embeds/native-document.html`) that page-context JS
   cannot reach into.

Switching to Playwright resolved all three, and surfaced three failure modes worth codifying:

- **A JS `.click()` is an untrusted event.** The LinkedIn viewer ignores it, so the page never
  advanced. 61 "pages" were downloaded that turned out to be **61 copies of the same image**
  (1 distinct MD5 across 61 files). The run reported `DONE: 61 page images` — a **false success**
  that would have been reported as fact had the output not been hash-checked.
- **Frame mismatch.** The post carried two documents (v2 156pp and v1 103pp). Clicking "next" via
  `page.frame_locator(...).first` while reading images from a different `Frame` object silently
  paged the wrong document.
- **The download WAS enabled all along.** Probing the frame's controls revealed a
  `Download document` button (hidden until fullscreen) and a direct
  `feedshare-document-pdf-analyzed` anchor. Fetching that with the authenticated context returned a
  valid 7.49 MB PDF with a **full text layer (209k chars, 156 pages)** — making page images, OCR
  and any AI-vision step entirely unnecessary.

The cheapest correct path was therefore the *last* one tried, not the first.

## Decision

**For any task that involves reading a document hosted in a third-party web viewer, use Playwright
first.** CiC remains available only with explicit per-task approval, consistent with ADR-124.

Attempt acquisition in this order, stopping at the first that works:

1. **Publisher-provided download/export**, if the publisher enabled it. Probe the viewer's own
   controls and anchors before concluding it is unavailable — controls are frequently present but
   hidden until fullscreen/hover, and absence from the page DOM proves nothing when the viewer is
   in a cross-origin iframe.
2. **Native per-page assets** (the viewer's own page images, typically higher resolution than any
   screenshot).
3. **Element-scoped screenshots of the document element only** — never full-page. Locate the
   document container (e.g. `div.document-s-container`, `iframe[src*='native-document']`) and
   screenshot that element.

Then, before any vision/OCR step: **check for a PDF text layer.** If present, extract text and stop.
OCR and AI-vision (including the $0 Gemini vision route) are a fallback for scanned documents only.

### Mandatory engineering rules

- **Never rely on `frame.evaluate(el => el.click())` to drive an embedded player.** Use a real
  Playwright click (`Frame.locator(...).click()`), which dispatches trusted events.
- **Scope the click and the read to the SAME `Frame` object.** Never mix `page.frame_locator()`
  with a separately-resolved `Frame`.
- **Select controls by their actual accessible handle.** In this viewer the label was element
  *text*, not `aria-label`; an `[aria-label="…"]` selector matched 0 elements.
- **Verify distinctness before declaring success.** Hash the captured pages; if
  `distinct < captured`, the capture is invalid and must be reported as failed. A page-advance that
  silently no-ops is the default failure mode of this class of work, and it *looks* like success.
- **Fail loudly on a stalled page-advance** rather than continuing to write duplicates.

### Authentication

Use a **dedicated persistent Playwright profile**. Copying cookies from the live Chrome profile may
work once but gets challenged by an authwall; a single real sign-in in the Playwright profile is
durable. Per the standing credential boundary, **the user performs the sign-in themselves** — the
agent opens the window, waits, and never types credentials.

### Copyright and publisher intent

- Acquire only through routes the publisher has enabled. Do not circumvent a disabled download.
- Produce **paraphrased idea-extraction notes**, not a reproduction of the work. Short attributed
  quotes where exact wording matters; never a page-by-page transcription.
- Retain the source file as a **project asset for internal use**; do not redistribute it.

## Consequences

**Positive** — the correct path is usually far cheaper than the first-reached-for one: a text-layer
PDF removed ~156 image reads, an OCR pass and an AI-vision pass at a stroke. Captured documents
become durable, re-readable project assets rather than transient screenshots. The
verify-distinctness rule converts a silent false success into a loud failure.

**Negative** — a one-time human sign-in is required per Playwright profile. Viewer DOMs are
vendor-specific and will drift, so selectors need re-probing rather than assuming.

**Neutral** — CiC remains legitimate with explicit approval, e.g. when a session is already
authenticated and the job is a handful of pages.

## Compliance

Reference implementation: `Documents/360lm-weberp/tools/ebook_pw.py` (DL) — subcommands
`login` / `probe` / `pdf` / `harvest`, including the frame-scoped click, the change-detection
guard and the persistent-profile login flow.

## Related

- **ADR-124** — Playwright-first for PWA screenshot/screencast automation (this ADR extends the
  same principle to third-party document viewers).
- Standing credential-entry boundary — the assistant never submits credentials, by UI or script.
