# Changelog

Source: https://getanalog.io/docs/changelog/

Notable, user-facing changes to analog-sdk.

Release notes for `analog-sdk`.

## 0.20.0 — 2026-09-16
### Added

- Capture a page from your harness's browser with the reusable helper from `analog browser capture-script --output PATH`. View the resulting capture file with `analog view` to extract, save, and search in one command, retaining its URL, visibility observations, and reported capture limits.

- `analog browse URL --find TEXT` fetches, saves, and searches a page in one command. Matches come from that capture, with its saved handle and capture notes; add `--json` for matching objects with full record values. Capture and save failures remain explicit and return a nonzero exit status.

- The ordered document view includes main prose beside record sections. A Markdown section with `role="main_content"` identifies the page's main prose region when available, at most one per result. Region plans expose `excluded_xpaths` for content omitted from their Markdown.

- A long fetch now survives a hard kill with something to show. While the built-in browser scrolls a slow or bottomless page, Analog periodically saves an in-progress partial to the results store; if the process is killed (SIGKILL — the shape of many harness timeouts), `analog history` shows that checkpoint, marked `[partial]`, holding the page's markdown as of the last save. Checkpoints pace themselves against their own cost (never before 15 seconds, backing off on heavy pages), fetches that finish clean them up, and interrupted runs' salvages now carry the same `[partial]` marker. Sweeps checkpoint too: a kill mid-sweep leaves the pages already merged. `history --json` gains a seventh field, `partial`.

- View captured HTML or saved results with `analog view`. Supply an HTML file and its browser URL to extract, save under a chosen name, and search in one command. Repeat `--find` for independent searches with shared evidence, or use `--save-only` for a receipt without page output.

### Changed

- `analog assess` no longer re-renders a page you fetched moments ago: when a result for the same URL was saved within the last 15 minutes, the command reuses its stored assessment — naming the result and its age, re-running only the live feed checks — and `--fresh` forces a full re-fetch.

- `analog assess` reports the measured page facts and available alternatives, including feeds, navigation, sitemaps, and pagination. The task-by-task decision guide is linked from the docs instead of repeated in every run. Result previews keep fit guidance opt-in.

- Use `analog view` to extract and search a page supplied by your agent's browser, then query the saved result locally. `analog browse URL` acquires pages with Analog's built-in browser when you need it. Quickstart and setup instructions lead with the supplied-page workflow and include the browsing fallback.

- `analog quickstart` walks through capturing a page in your agent's browser, viewing it with Analog, and searching the saved result. It includes the built-in browsing fallback, an optional page overview, and a separate Python alternative.

- With guidance requested, collapsed-section advice names the appropriate expansion option: `expand_all=True` in Python or `--expand-all` in the CLI.

- `analog browse` now returns a saved-result receipt with material capture notes. Search the saved content directly or request an overview with `analog view`; `analog --guidance browse` adds an optional next action. `--no-save` requires `--json` or `--markdown`, and full preview fields are selected with `analog view --complete`. Failed saves and abnormal pagination stops return failure statuses while preserving available data and identifying readable partial captures.

- Result previews and pagination descriptions keep coverage facts visible while making next-step guidance opt-in. Use `guidance=True` in Python or `analog --guidance view <handle>`; `--complete` remains an independent choice of preview fields.

- Page and section previews show field names, types, list shapes, and sparse-field coverage. Inspect values with `analog describe` or `field_stats()`; `--complete` includes hidden fields in the summary.

- Result previews list record and Markdown sections in document order, including navigation. Each record section shows one compact sample, with multi-entry list values collapsed to a count (`[6 values]`); full values remain in `records` and exports.

- Page sweeps (`--pages`) are labeled experimental in the CLI help. Compatible sections pool across pages, with missing fields represented as null and disclosed; sections that cannot be pooled remain separate.

- Sweep requests no longer carry reuse tokens: the `reuse_token` and `offer_reuse` request fields and the `reuse_token` response field are retired (schema 29). Every page of a sweep extracts independently; a token sent by an older client is ignored harmlessly, and sweep results are unchanged.

- Page sweeps now pool sections across pages that differ in fields: a pooled section keeps the union of its pages' fields, records from a page without one of them are null there, and the affected fields are named per section and on `page_sweep.page_partial_fields`. Sections that genuinely diverge still stay separate, and the repeated-record disclosure now also counts an item re-served in a different shape inside a pooled section.

- Preview truncation earns its keep: values over the display cap by less than the marker's own length now render whole, the marker is a compact `…(+N chars)`, and the where-to-find-full-values pointer appears once per preview instead of inside every shortened value.

### Fixed

- A `pages=N` sweep's note now places the page's own collection size beside the merged record count — "209 records merged across the batches. The page's own heading puts the collection at 483." — so a sweep that covered part of a collection never reads as the whole of it. The count is read from the first page, from its pagination range ("1-60 of 2,595") or, new, from a count heading ("483 jobs"), and arrives structurally as `page_sweep.stated_total` with `stated_total_source` naming which. Single-page results gain the same `pagination.stated_total_source`, and their note names the source the same way.

- Improved extraction of people directories.

- Requested page overviews now include Markdown regions in reading order, while preserving record section indices and keeping full text opt-in.

- An in-place pagination batch that extracts no records no longer ends the sweep. The remaining captured batches keep merging, the barren batch is disclosed by address in `page_sweep.pages_without_records` — the same disclosure link sweeps already carry — and its text stays in the merged markdown. Previously such a batch stopped the sweep as if the collection had ended, silently discarding the batches after it.

- Later batches of an in-place pagination sweep (`pages=N` on a page whose pagination controls operate without URLs) no longer lose field structure. Every batch capture now receives the same visibility treatment as the first, so records keep their first-batch shape — separate title and URL fields instead of one fused text field — across the whole sweep.

- In-place pagination sweeps (`pages=N` on script-driven controls) now carry true per-batch provenance. Each batch extracts against the address the page showed when it was captured, each markdown banner names that address, `page_sweep.page_urls` records the address trail the page's own controls produced, and every retained record carries its source in the field named by `page_sweep.source_page_field` — the same provenance link sweeps already stamp. A widget that pages without moving the address shares its one address throughout.

- Bottomless pages no longer pay every scroll strategy's full cap in sequence. Once one strategy reaches its cap with the page still growing, the fetch says so immediately — "page keeps growing at the scroll cap — likely bottomless" — and the remaining strategies run as brief probes, cutting minutes from endless-feed fetches. Raise `max_scroll_iterations` to scroll deeper deliberately.
- An interrupted fetch now saves what it had. A polite termination (SIGTERM — the shape of an agent harness's tool timeout — or the interrupt an agent sends) stops the fetch at a safe point, captures the partially loaded page, and saves it as a markdown-only artifact before the run exits with the new uniform exit code 130; `analog history` shows the partial instead of nothing.

- The built-in browser now captures the full content of pages that keep fixed-size placeholders for off-screen list items (long forum threads are the common case): previously such pages could capture with most items empty. Scrolling these pages also no longer accumulates repeated copies of the page's own chrome into the capture.

- Long virtualized threads capture more completely: the browser now paces its scroll to a page's own loading (waiting for reserved placeholder rows to fill), starts from the top when a page opens deep-linked partway down, revisits rows it missed, and no longer duplicates a page's repeated chrome — such as skip-navigation links whose text tracks your position — into the capture.

- Plain HTTP fetching now reads gzip-compressed responses correctly, preserving charset handling and the decompressed response-size limit.

- Markdown now resolves links against the address that served the page: `result.markdown`, section and document-view Markdown, and every swept page's segment emit absolute destinations for relative anchors — the same targets the records surface already carries — so links in the markdown are usable without re-deriving the base URL. HTML supplied without an address renders as authored.

- Numeric values remain available when only some pages in a page sweep provide them, including when the first numeric values appear on a later page.

- Shape-conflict errors on `export` and `distinct` now give remedies you can actually run: they name the exact `--section` values that would work and never re-prescribe a flag already on the failing command line.
- `export --fields`/`--where` over sections of different shapes still answers, and now says so: the pooled output carries a conflation caveat on stderr naming the pooled sections and a per-section path — the same caveat `distinct --count` already carries.

- A page that re-renders a complete list while the built-in browser scrolls it — a storefront grid redrawn by a merchandising script, with fresh nodes and a new order — is no longer captured with every item twice under a `windowed_feed` note. The capture keeps the page's own list once; the windowed-feed disclosure and reconstruction remain reserved for feeds that actually drop earlier items as they scroll.

- Deep copies made with `response.model_copy(deep=True)` preserve record-section relationships and retained Markdown, so copied responses can be saved and reopened.

- Virtualized streams now capture completely even when the page re-renders chrome above them mid-scroll (a notice bar, an ad slot, a loading indicator): the browser keeps following the same list through the layout change instead of silently losing most of what it had already collected. Fetches of such pages are also substantially faster — a fixed scroll-position indicator no longer reads as content still loading, which previously added a multi-second pause to every scroll step.

- Multi-page sweeps no longer discard Collections: a Collection formed on any swept page carries onto the merged result, pooling membership across pages, so collection views and `analog export --collection` work on swept results that connected one.

- An interrupted `pages=N` link sweep now keeps the pages it already swept. Ctrl-C or a supervisor's termination stops the sweep at a safe point: the swept pages stay merged, the result disclosed (`page_sweep.stop_reason: "interrupted"`) and saved — `analog history` shows it — and the run still exits with code 130. Previously an interrupt discarded the whole sweep, and a termination request landing mid-page could be silently swallowed while the sweep kept fetching.

- Sections merged across a sweep's pages or batches no longer keep a group heading some of their records contradict. A pool whose members arrived under different headings is now unlabeled — the same agreement rule the heading's link already followed — and per-record area labels keep each record's own group where the page provides them.

- A withheld area is now named in the result preview — its heading or location plus item count, with a pointer to the markdown that still carries its text — instead of the anonymous "N other areas detected but withheld" count. The count-only note remains only for saved results whose artifact carries no richer evidence.

## 0.19.0 — 2026-08-19
### Added

- `analog export --collection INDEX` exports the Collection a result prints
  as `Collection N` — its canonical records over exactly its connected
  sections — and `--fields`/`--where`/`--sort`/`--limit` shape it as one
  table. `analog get`'s query hint addresses the Collection when one exists,
  `analog describe` lists Collections with that selector, and the pooled-export
  refusal names it.

- `assess` now reads the syndication feeds a page declares in its head (`<link rel="alternate">` RSS, Atom, or JSON Feed) and names them in the guidance — read from the HTML, never fetched. `FitAssessment.declared_feeds` carries them structurally.

### Changed

- Response schema v27 adds the required `document_section_plan`; `result.sections` follows that authored order, reuses each record `Section`, and keeps unresolved prose regions in place with empty Markdown.

- Markdown output now uses ATX headings and stable hyphen list markers,
  preserves semantic blocks and irregular tables more faithfully, and safely
  serializes authored code fences, links, images, and media destinations.

- Markdown output adopts Marcato 0.3: single-cell layout tables unwrap to
  prose with their line breaks intact instead of rendering as one-cell tables
  full of literal `<br>` text.

- `preview()` now summarizes a page's navigation sections in one aggregate line — section count, total links, and their placements (header / in-body / sidebar / footer) — instead of rendering their records inline. Up to two labeled in-body navigation sections (a table of contents, a curated link list) still appear compactly at their position in the reading order, and on a page that is entirely navigation the summary line leads. The full link records remain in `structured_content` and one `sections_by_kind("navigation")` call away; nothing is dropped from the result.

- Navigation is now part of `structured_content`: header, footer, sidebar, and in-page link groups arrive as sections of kind `"navigation"` at their position in the reading order, one record per link (`text`, `url`, `group`, `depth`), with `Section.placement` saying where each sits. The separate top-level `navigation` field is gone; `sections_by_kind("navigation")` collects the whole map, and `find` covers links like any other record.

- `preview()` now says when a page's substance is prose rather than records. On a page whose mixed `sections` view carries at least as much rendered text in Markdown Sections as in record sections, the preview adds one framing line — how many markdown regions, how much text, and that they ride beside the record sections in `sections` in reading order — followed by the full-page markdown pointer. A not-extracted outline entry whose region's text renders in the mixed view now says so directly instead of reporting only why it produced no records, so an article-shaped page with a few incidental record sections can no longer read as fully extracted. Nothing changes in the result itself; records, `sections`, and `markdown` carry exactly what they did before.

- Results no longer expose the `bot_protection` classification. When no structured Sections are extracted, the preview now states that observed outcome without guessing why and keeps the page document available.

- Virtualized-feed reconstruction now rebuilds items the page only ever showed as pre-hydration placeholders. When a feed item's sole captured state renders its content as escaped source markup — the page shipped the item's own markup but never hydrated it while scrolling — the capture now carries that content as real elements, links included, inside the item's preserved wrapper. Items whose text merely mentions markup are never rewritten. The windowed-feed disclosure counts how many items were rebuilt from their shipped source.

### Fixed

- The built-in browser now refuses redirects, client-side navigation, subresources, and WebSockets that target non-public network addresses before connecting. `allow_private=True` remains the explicit opt-in for trusted development and intranet pages.

- The built-in browser no longer prints internal `TargetClosedError` tracebacks to stderr when a page finishes loading late resources during teardown; stderr carries only what the command has to say.

- Comparison sections now have working markdown on both surfaces: a saved result's per-section markdown renders the authored feature table (plans as columns) instead of coming back empty on grid-built pages, and the whole-page markdown carries values that pages draw as icons with a declared name — a checkmark cell's "Not available" appears as text instead of a blank that reads as authoritative absence.

- `describe`'s table now shows whether a field behaves numerically in queries, and in what unit — `numeric(seconds)` beside the field's traits — so the facts that govern how `--where` reads a comparison are visible on the surface you actually read before writing one, not only in `-f json`.

- `distinct --count` across sections with different shapes now carries the same caveat the value list enforces: the count still answers, with a stderr warning that the pool can conflate unlike values and how to scope per shape — and the value list's refusal no longer teaches `--count` as if it were caveat-free.

- A page that the server answers with an error status (a 404, a 410, a 5xx) is now a fetch error naming that status, through the built-in browser exactly as through the HTTP fetcher — never rendered and presented as content. `FetchStatusError` (a `FetchError`) carries the status and URL; blocked-request statuses keep their separate `FetchBlockedError` framing.

- `find` now covers everything the markdown export shows: a section whose records dropped some content still reports a match from the section's text (saying that's where it was found), and content between extracted regions is reported from the page markdown as a last resort — a string you can see in `export -f md` is never answered with "No matches" again.

- `get` (and `analog()` in auto mode) now leads its preview with the platform's own data feed when the page looks like a Shopify store or Greenhouse board — naming the collection's feed on a collection page and routing to `assess` for the check. Read from the page, no extra request; previously only `assess` mentioned the feed.

- A page whose only pagination evidence is a head `<link rel="next">` is no longer reported as a confirmed paginated collection, even when the link's URL carries a page number — the declaration can't corroborate itself, and infinite-scroll feeds emit exactly this shape. The link is still reported and followable; the confirmed claim and its `pages=N` sweep guidance now require a second, independent signal (rendered pager links, a stated count, or in-place controls). The unconfirmed disclosure also no longer asserts "these records cover this page only", which an infinite-scroll capture spanning many pages would contradict.

- The hidden-fields footer in `analog get`/`analog open` output now names a complete runnable command — `analog open <handle> --complete` for saved results, "re-run with `--complete`" otherwise. Previously it said only "pass `--complete`", and the flag exists on `get` and `open` but not on `describe` or `export`, so the bare hint invited pairing it with commands that reject it.

- `load_all` can no longer make a result worse than not asking for it: when pressing a load-more control destroys content instead of adding to it (a virtualized timeline resetting itself, say) or leads away from the page, the capture keeps everything gathered before the press and the result says what happened — instead of saving the collapsed page and advising you it was small.

- Page content whose text merely looks like a load-more control — a forum topic titled "Load more button", say — no longer produces a load-more suggestion: `load_all` is offered only when the page has a real control to press.

- Offline logout warnings now print a runnable command for revoking the still-active key from another signed-in machine.

- A "next" link that leads to a different document (a documentation site's next chapter) no longer confirms a paginated collection, so `--pages` is not offered on such pages and a sweep will not merge the next chapter into this page's records. Pagers whose next link steps to a later page position keep confirming.

- Preview guidance no longer points to a `page_section` field when the result does not provide one.

- A pagination sweep no longer stops with "a page re-served only records already merged" when a swept page simply produced no records — the sweep now continues to the later pages and names each record-less page in its disclosure (their text stays in the merged markdown). Separately, a page whose only real pagination evidence is a declared next document is no longer reported as a confirmed collection just because unrelated page content mentions "load more" — pagination is only read from links that actually carry a page number, as always documented.

- Previews no longer tie two page-level disclosures to whether sections extracted. The single-subject withholding note now appears even when unrelated sections carry records (previously any extracted section silenced it), and zero-record results now include the page-outline block (previously the outline rendered only beside extracted sections — exactly where an empty result most needs to distinguish "not on the page" from "on the page, not extracted").

- The preview's prose framing ("Most of this page's text is prose, not records") no longer lets navigation sections weigh against the prose: a link tree is the page's map, rolled up in its own tier, not the records the prose is compared with. Long documentation pages whose header, sidebar, and footer link trees tipped the balance now frame honestly and point at the markdown.

- Captures of virtualized feeds no longer accumulate repeated copies of the same content: when a page re-renders as it scrolls (a live thread, a windowed timeline), the capture now keeps the first serving of each piece of content instead of concatenating every intermediate state — so record counts over such pages reflect the page, not the number of times it re-drew itself.

- Virtualized-feed captures no longer keep two copies of one item when the page serves it in different render states during the scroll — a pre-hydration placeholder whose content is the item's source markup as escaped text beside the fully rendered item. When a feed declares item identity on its rows (`data-id`), the reconstruction now keeps exactly one copy per item, preferring the fully rendered state wherever one was observed; an item the page only ever showed as a placeholder is still captured rather than dropped. Counts over such feeds stop double-counting, and escaped markup stops leaking into text.

- A reveal click that lands on a different page no longer hijacks the capture: when a "show everything" control turns out to route elsewhere — a careers page's "See all articles" leading to the blog, say — the result now describes the page you asked for, not the destination.

- `assess` on a Shopify collection page now checks the collection's own product feed (`/collections/<handle>/products.json`) instead of the store-wide one, so the count it reports is the collection's; if the collection feed doesn't answer, the store feed is checked and the guidance names both. Probes and the fit now read the URL that served the page after redirects.

- A `--where` comparison that can't mean what it looks like now says so: a value that doesn't read as a number ("13:00"), a field none of whose values do, or a value written in a different scale than the field's unit (matching every row or none) each get one stderr line naming the field's scale — instead of returning zero rows or the full table as if the filter had worked. Exit codes are unchanged.

## 0.18.0 — 2026-08-05
### Added

- `analog doctor` checks whether the environment can run Analog end-to-end — credential, outbound network, backend, and the built-in browser — and says what to do about whichever failed. It distinguishes an environment that blocks network access (common in sandboxed agent harnesses) from a site refusal or an Analog outage, and fetch failures that look transport-shaped now point to it.

- `analog update` upgrades Analog through whichever channel installed it — uv tool, Homebrew, and pipx installs run the upgrade directly; a pip-managed environment gets the exact command printed instead of run. `analog update --print` emits the command alone for scripts.

- Added `analog login --no-browser` for interactive remote shells and containers: finish email verification in a browser elsewhere, then paste a one-time handoff at the hidden terminal prompt. The credential remains stored locally and is never displayed.

- `analog describe -f json` emits the field-stats as a JSON array of the selected sections — section index, label, kind, record count, and each field's stats (type, source, multivalued, traits, hidden, numeric, unit, coverage, cardinality, sample) — completing the machine-readable form of the query workflow.

- `analog history --json`, `analog find --json`, and `analog distinct --json` emit machine-stable JSON: history as an array of entries (handle, url, saved_at, name, sections, records — newest first), find as exactly the objects the Python surface's `.find()` returns, and distinct as `{value, count}` pairs most frequent first (with `--count`, the bare number). Empty results are `[]`, never prose.

- Page outlines now mark regions preserved in full-page markdown but not emitted as structured records as `markdown_only`, distinguishing unstructured text from missing content.

- Results now present record and Markdown sections together in page order, so prose remains directly inspectable and searchable alongside extracted records.

- Results disclose when a single-subject reading of a page was weighed and not returned: the new `subject_withheld` field carries `conflicting_subjects` (the page presents more than one candidate subject), `declaration_unverified` (the page declares a subject the visible page doesn't corroborate), or `below_evidence_bar` (too little verifiable evidence for a record we'd trust), and stays unset on pages where nothing was withheld. Empty-result previews state the reason in plain terms.

### Changed

- `Section.xpath` now returns an ordered list of exact locations when a section draws from several page regions, and `Section.markdown` renders those regions together. Single-region sections still return one XPath; unresolved locations remain `None`.

- Every upgrade nudge — the status floor and schema warnings, the sign-in failure hint, the wire-schema-skew error — now says `analog update`, which resolves the right upgrade for the install it runs in. Previously the nudges named one channel's command, which could be wrong advice in another channel's install.

### Fixed

- Checking whether the built-in browser is installed no longer prints stray teardown errors to stderr — the check reads playwright's own metadata instead of starting its driver, so `analog doctor`, `analog login`, and `is_browser_installed()` stay silent. An interrupted browser download now correctly reads as not installed.

- Corrected `analog export` help to describe `--kind` as selecting kinds present in a saved result; it no longer promises a `content` kind that many results do not provide.

- `analog feedback quality --help` now states the 2,000-character note limit.

- `analog help --help` now answers like every other command instead of erroring, the exit-code table appears in every command's `--help` (it previously showed on only some), and `analog whoami` states its `--verify` default.

- When `analog logout` can't reach the backend to revoke the key, the warning now states the consequence plainly — the key stays active server-side until revoked — and prints the exact `analog keys revoke …` command, with this key's own prefix, to finish the revocation from any signed-in machine. The local credential is still cleared either way.

- Markdown now preserves the extent of page-authored notes, warnings, and other advisory blocks, so following prose remains outside them.

- Full-page Markdown now renders animated counters as one current value instead of concatenating every transition frame.

- The page outline now names a labelled region that carried items but produced no records — a sidebar of opening hours, say — instead of folding it into the one-line structure summary, where a whole region could be missing from both the records and the output. Page chrome stays summarised as before.

- A page whose only pagination evidence is a declared next document — the way a documentation page or blog post names its neighbour — is no longer reported as page 1 of a paginated collection, and no longer suggests a multi-page sweep that would merge a separate document into the result. The next link is still disclosed, and pages that confirm a collection keep the full disclosure and the sweep. Results carry the distinction as `collection_confirmed`.

- `analog get`, `analog history`, and `analog open` now show the URL that served a page when a redirect changes its path, query, host, or port.

- Section-scoped Markdown exports now retain table column headings.

## 0.17.2 — 2026-07-27
### Added

- Added `analog quickstart`, a network-free guide to connecting an account and making a first extraction from the shell or Python.

### Fixed

- Markdown keeps authored bold and italic emphasis in normalized document
  views.

- Browser calls made directly on an active `asyncio` event loop now teach the working `asyncio.to_thread` integration instead of suggesting an async SDK that does not exist.

- Browser captures complete collections from scrollable page regions instead of stopping at the initially visible rows.

- Heterogeneous results now point a refused `to_dataframe()` call to the working per-section `structured_content[i].to_dataframe()` recovery instead of switching Python callers to raw records.

- `analog find --field` now searches saved results with different section shapes without failing on sections that do not contain the selected field.

- Fit guidance recommends structured extraction without promising complete records before the result is available.

- Multi-page fetching now advances through a page's own numbered next-page link when no separately labeled next link is available.

- Multi-page link sweeps now put each retained record's source URL in `source_page_url`. `page_sweep.source_page_field` identifies the exact field when an authored name requires a collision-safe suffix.

- Pagination disclosures and fit guidance now use Python-native `pages=` wording in SDK results and shell-native `--pages` wording in CLI output, with a valid page count instead of mixed syntax or a placeholder.

- Saved auto-mode results now retain their fit recommendation and preview guidance when reopened.

- CLI private-address refusals now name the actionable `--allow-private` flag, while Python exceptions retain `allow_private=True`.

- Extraction quality improved across editorial, commerce, and reference pages.

- Duplicate saved-result names now report what happened, offer recovery choices, and expose the conflicting name on `ResultNameInUseError.name`.

- Python discovery on fit assessments, saved-result metadata, and Collections now leads with their documented data and operations instead of inherited compatibility methods.

- Python's built-in `help()` on returned model classes now leads with their documented fields and operations instead of hundreds of inherited Pydantic internals.

- Runtime guidance now composes real saved-result references and uses prose instead of placeholder commands when a value must be chosen by the caller.

- Saved-result failures now derive from `AnalogError` while retaining their existing `KeyError` or `ValueError` behavior.

- Saved-result schema errors now give Python-native recovery guidance in library calls while retaining command-specific guidance in the CLI.

- `Section.find(..., field=...)` now raises `KeyError` for unknown field names instead of returning the same empty result as a valid search with no matches.

- Python's `analog()` now rejects unsupported mode values before fetching, extracting, or saving.

## 0.17.1 — 2026-07-26
### Fixed

- **Browse disclosures persist with the saved result.**
  `result.browse_actions` — the interactions the fetcher took or
  offered to reach the content (a reveal-all click, a "Load more"
  control present but not pressed) — now saves with the result and
  appears in the JSON envelope, so `analog open` re-prints the same
  `Browse:` lines the original fetch showed and a cached result still
  discloses what the capture did and didn't cover. It is `None` on
  artifacts saved before recording existed; those open exactly as
  before.

- **`analog status` leads with the fact you act on.** The backend line
  now reads `ok (schema N, build xxxxxxxx)`: the wire-schema number —
  the compatibility contract — comes first, and the deploy identifier
  is shortened to eight characters under an honest `build` label
  (previously forty characters labeled `version`, which promised
  something interpretable and delivered noise). Pasting `status`
  output into a report still pins the exact deploy.

- **Package completion shows the public API.** `dir(analog)` now follows
  the curated `analog.__all__` surface, so IDE completion no longer
  mixes public classes and functions with incidental submodule names
  or the `annotations` future-import marker. Existing imports are
  unchanged; this only cleans up discovery.

- **`history` says when.** Each row now shows how long ago the result
  was saved (`just now`, `5m`, `2h`, `3d`, `6w` — the units
  `rm --older-than` takes), and rows align whether or not a result has
  a friendly name. Two saves of the same URL used to be
  indistinguishable except by handle.

- **Icon-drawn labels and legend terms render what the page declares.**
  The declared-text contract icon-only controls and table cells already
  had now covers non-interactive elements too: a support level drawn as
  an icon, a legend term whose only content is an icon — anywhere the
  page declares the meaning as clipped text or a `title` attribute, the
  markdown renders the page's own words instead of nothing. Visible
  text always wins, and nothing is ever invented. One refinement from
  the same contract: when a page renders an icon link and a text link
  to the same destination side by side, the markdown keeps one link
  instead of printing the name twice.

- **A typo can't pass as an empty result.** `--where`, `--sort`, and
  `find --field` now exit `1` when the named field exists nowhere in
  the result (previously a stderr warning with exit `0`, so a typo'd
  filter looked exactly like a true zero-match to anything reading
  stdout). Filtering or sorting on a field that only *some* sections
  carry is unchanged: known name, match-nothing / sort-last semantics.
  And a valid filter that matches nothing now still emits the CSV
  header row — "ran, zero matches" is visible output, not an empty
  string.

- **Python `find` rejects unknown fields too.**
  `result.find(pattern, field="...")` now raises `KeyError` with the
  available names when no section carries that field, instead of
  returning `[]` as though the pattern had no matches. A field present
  in only some sections remains a valid restriction.

- **The taught query runs.** The `Query it:` line `analog get` prints
  now composes a command that succeeds on its own result: on a
  multi-section page it projects fields every section shares, or scopes
  itself to one section when nothing is shared. Previously it could
  suggest a pooled export that export itself refused.

- **Pooling remedies match the result.** When `result.records` or a
  pooled serializer refuses sections with different fields, its error
  always points at the single-section accessor and only suggests
  `sections_by_kind(...)` when the result actually carries kinds. The
  suggested calls name those real kinds; a kindless result no longer
  teaches a call that can only return an empty list.

- **The divergent-record note names your handle.** The review recipe it
  prints (`analog open … --complete`) now carries the saved result's
  actual handle instead of a literal `<handle>` placeholder — the
  suggested command runs as printed.

- **Withheld areas are named.** When extraction withholds a page region
  it couldn't do justice to, the result's preview now names each
  withheld region — its heading label when the page gave one, its
  location otherwise, with item count — wherever it sits in the page
  skeleton. Previously a region buried inside other non-extracted
  structure was disclosed only as an anonymous count, so an agent
  couldn't tell whether its answer lived inside. Results saved without
  a page skeleton keep the count-only note.

- **`assess` recognizes a saved result when it's handed one.** Passing a
  handle, name, or `latest` (natural, since every other read command
  takes them — but `assess` measures live pages, so it takes a URL) now
  explains just that and names the saved result's own surfaces
  (`analog open` / `analog describe`) plus its source URL for a live
  re-assessment. Other non-URL input gets a usage error teaching the
  expected shape. Previously both surfaced a fetcher scheme error with
  the fetch-failed exit code.

- **Python separates invalid URLs from fetch refusals.** `analog()` and
  `assess()` now raise `ValueError` before fetching when their URL is
  not HTTP(S), including when the caller supplies `html=`; they never
  read a `file://` target. A well-formed private or loopback URL remains
  a policy refusal in the `FetchError` family, and its precise
  `UrlNotAllowedError` is now importable from `analog`. `analog get`
  reports a non-URL as a usage error instead of a failed fetch.

- **Saved-result references are consistent in Python.** The library's
  `open`, field mutation, metadata, update, rename, and delete functions
  now accept the same three forms as the CLI: a handle, friendly name,
  or `latest`. A miss raises `ResultNotFoundError` explaining those
  forms and pointing at `analog.history()`; deleting a well-formed
  missing handle remains an idempotent no-op. Previously `rename`
  minted names that most library functions rejected, and `delete` on
  one silently did nothing.

- **One printed spelling per section id.** Previews now say `section[N]`
  everywhere `describe` and `find` already did; `get`/`open` previously
  printed the longer `structured_content[N]` for the same section. Input
  is unchanged: both spellings, bare indices, and labels remain accepted
  by every `--section`, and every `--section` flag's help now teaches
  the printed spelling.

- **`describe` keeps fields tellable-apart under a giant shared
  prefix.** When every field in a section starts with the same long
  run (a table caption folded into each name can top 100 characters),
  the field column used to truncate all rows to one identical string.
  The shared prefix now prints once above the table and rows show the
  distinguishing tail (`…_month`, `…_apr`); a full field name is the
  printed prefix plus its tail. Sections with ordinary names are
  unchanged.

- **Refusals teach only remedies you can run.** When a pooled export is
  refused because sections have different shapes, the error now names
  `--kind` only when the result's sections actually carry kinds (and
  lists them), and its `--fields` example uses real columns the
  sections share — never placeholder names. Nothing changes about what
  is refused, only that every suggested way out works.

- `analog export -f markdown` now works as an alias for `-f md` — the
  spelling the rest of the product uses when it talks about this
  output.

- **One mistake, one exit code.** Naming a field the result doesn't
  have now exits `1` everywhere (the same class as an unknown handle:
  the command was well-formed, the data lacks the name). Previously
  `export --fields` reported it as exit `2` while `distinct` said `1`.
  Exit `2` remains reserved for malformed invocations. Both errors now
  also point at `analog describe <your ref>` with the ref filled in —
  the suggested command runs as printed. A pooled export refused
  because sections have different shapes is the same class: it now
  exits `1` too, without the usage banner it used to print.

- `dir()` on extraction results and sections now shows their documented data, reading, and export surfaces.

- Runtime preview and describe truncation notes now include a runnable export command for the saved result.

### Added

- **`diff` answers "did the content change?", not just "did the shape
  change?".** Each matched section now reports record turnover —
  records present in only one of the two saves, compared by
  whole-record content — with up to three examples named by an
  identifying field when the section has one (a feed's `title`, a
  grid's `name`). Two saves whose counts matched while the records
  rotated used to read as "nothing changed". Records carry no stable
  key, so a record that changed any value counts once on each side;
  the schema and field-stat views are unchanged.

- The original `analog hint` crystal can now be replayed as `analog hint 001`, and `analog hint --list` shows available animation IDs.

- `analog hint 002` adds a traveling-defect animation to the local terminal-art gallery.

- `analog hint` now draws the landing page's emerald crystal in the terminal.

## 0.17.0 — 2026-07-25
### Changed

- Extraction improved on platform status pages and grouped multi-day
  schedule pages.

- **Markdown now reflects the rendered page only.** Content that was
  not displayed at fetch time — dismissed cookie dialogs, closed
  drawers and menus, hidden template and variant blocks — no longer
  appears in any markdown surface (`result.markdown`, per-section
  markdown, `-f md` exports). Markdown of template-heavy pages gets
  substantially smaller, with the visible content untouched; the
  document title is still included, and `assess`'s measured token
  estimate shrinks with it.

- **Field schemas follow the page.** A section's `fields` now lists its
  fields in the same order as its records — the page's own order for
  comparison and schedule sections, most-relevant-first elsewhere —
  instead of alphabetically, so `describe` and schema listings read in
  record order. Saved results keep the order they were saved with.

### Fixed

- **Icon-only controls render what the page declares.** A status
  icon, an icon-drawn button, or an icon-only link whose meaning the
  page declares in its accessibility tree (`aria-label`, SVG
  `<title>`) now renders that declared text in markdown — a status
  page's component list reads `APM … Operational` instead of a bare
  name, and an icon-only link keeps its destination with its declared
  text instead of disappearing. Visible text always wins over a
  label, `aria-hidden` content stays silent, and nothing is ever
  invented — the same contract icon-only table cells already had.

- **Markdown discloses values the page draws only as icons.** A
  comparison column whose values exist only as unnamed icons (a
  checkmark matrix, say) renders blank in any text form —
  indistinguishable from "no value". Every markdown the CLI emits
  (`export -f md`, per-section export, `get`/`open --markdown`) now
  carries the same withheld note the records preview shows: the
  affected columns, and that a clipped screenshot of the region will
  show them. A per-section markdown export also repeats the preview's
  plain withheld-columns line, since its comparison table is built
  from the records. `assess`'s comparison guidance now points at the
  withheld note instead of a blanket "read the page as markdown".
  Results without withheld columns are unchanged.

- **Links in records resolve against the page that answered.** When a
  fetch is redirected, the document's relative links — record URL
  fields, and the pagination disclosure's next link — now resolve
  against the URL that actually served the page, not the address you
  requested. In a `pages=N` sweep, each followed page resolves against
  its own serving URL, and the sweep's page list names the pages as
  served. Previously a page served from another host produced record
  URLs anchored to the requested host.

- **Comparison sections now print as the page authored them.** A
  comparison section — a pricing or edition matrix with few records and
  many fields — used to preview as one unreadable line per sampled
  record, and its markdown lost the comparison entirely. `preview()`
  and `analog export -f md --section` now render these sections in the
  page's authored orientation: features as rows, one column per record
  (headed by each record's `table_column` value), with the page's own
  row-group headings preserved. `records`, `fields`, and every
  structured export (json/csv/yaml) are unchanged; ordinary sections
  keep the record orientation.

- **Section ids round-trip.** `--section` (on `describe`, `export`,
  `distinct`, and `diff`) now accepts the id forms results print —
  `structured_content[N]` and `section[N]` — alongside labels, bare
  indices, and `all`. A heading label shared by several sections now
  selects all of them instead of silently the first.

- **Markdown tables now carry the declared text of icon-only cells.** A
  comparison matrix that draws boolean cells as icons (checkmarks)
  declares their meaning accessibly (`aria-label`, SVG `<title>`); the
  markdown view now renders those declared words instead of empty cells,
  so a feature column no longer reads as if no plan offers it. Cells
  with no declared text anywhere remain empty, and `aria-hidden`
  content stays silent.

### Added

- **Results now name the URL that actually served the page.** When a
  fetch is redirected, `result.resolved_url` carries the final URL (it
  is `None` when you supplied the HTML yourself), and it persists with
  the saved result, so an agent can cite the source it actually read.
  When the serving host differs from the requested one — the case that
  changes a citation — the fetch log says so and `analog history` names
  the serving URL on that result's row. The requested URL remains the
  result's identity everywhere else.

- **`Section.locate_in(html)` — find a section in a different rendering
  of its page.** A result's xpaths address the document the extraction
  read; a live DOM after scripts ran, or a re-fetch, can shift positional
  paths. `locate_in` finds the section there by its own record values and
  returns matches with xpaths valid in the document you passed. One match
  is a confident location; several mean the page holds several full
  copies of the content (a duplicate widget, a mobile/desktop double
  render) — pick with what only you can see, such as which copy is
  visible. An empty list means the section couldn't be pinned — never a
  silent guess at a wrong element. The preview's visual-withheld hint now
  points at it for screenshot clips.

- **Results now carry the page as Analog read it.** `document_view` ships
  the exact document the response's xpaths address, as a compact verified
  patch against the HTML you submitted; `attach_html` reconstructs it
  automatically. Section, outline, and navigation xpaths now resolve
  reliably on pages where a plain re-parse used to come up empty or
  silently land on the wrong element — so per-section markdown stops
  coming back empty on those pages, and markdown no longer includes
  markup that plays no part in what a reader sees. When the view is
  absent (an older service) or fails verification, everything degrades to
  parsing the submitted HTML as-is. Wire schema 24.

- **`page_sweep.divergent_sections`** — a multi-page sweep now discloses
  when a later page or batch contributed a section whose fields matched
  no section captured earlier: the count arrives in `page_sweep`, the
  affected section carries a `merge_note` saying why it stands alone,
  and the preview adds a sentence warning that no single field spans
  every merged record. A sweep that fragments is never presented as one
  uniformly queryable collection.

## 0.16.1 — 2026-07-23
### Fixed

- Pages that fold detail behind in-page disclosure links now report the
  available content under `browse_actions`; `expand_all=True` reveals it
  before capture. Ordinary navigation links remain untouched.

## 0.16.0 — 2026-07-23
### Added

- **Feedback submissions now return a durable receipt.**
  `Client.submit_feedback(...)` exposes the opaque `report_id`, and both
  `analog feedback` commands print it as `Receipt:` so a submission can be
  cited during later triage. The ID is not a report-reading endpoint.

- **Built-in browser recipes are now replayable.** `Browser.recipe` records
  every serializable setting that shaped a fetch, and
  `Browser.from_recipe(...)` reconstructs it. New browser-backed saved results
  retain that recipe in `fetch_settings.browser_recipe`; older saves remain
  readable and state that the exact recipe is unknown.

- **Compatible page areas can now arrive as a `Collection`.**
  `result.collections` preserves the physical `Section` indices and identity
  field while providing one canonical local records view. Repeated editorial
  placements remain available through the member Sections; Collection records
  select one whole authored placement and never fuse values across placements.
  A complete Collection powers `result.records`, CSV, YAML, and DataFrame
  export without duplicating repeated placements. Wire schema 23.

- **`page_sweep.cross_section_repeats`** — a multi-page sweep now
  discloses when the same records appear in more than one section of the
  merged result (two layouts of one collection that stayed separate
  sections), matched by a shared per-record URL. The preview adds a
  sentence when it's nonzero, so a "duplicates dropped" count is never
  read as "all duplicates handled."

### Changed

- **The serializer family is now methods, matching `to_dataframe()`:**
  `result.to_json()`, `result.to_yaml()`, `result.to_csv()` replace
  the `result.json` / `result.yaml` / `result.csv` properties. A
  serialization renders output (`to_json()` includes the full markdown
  rendering), so it reads as the call it is — and `.json` no longer
  collides with Pydantic's inherited `json()` method, which broke
  generic Pydantic tooling's expectations. No compatibility aliases;
  update call sites by adding `to_` and parentheses.
  `.markdown` stays a property: it is the page's content, cached after
  first render, not an export.

### Fixed

- The built-in browser now preserves visible page content rendered inside open
  web-component boundaries instead of returning an empty host element.

- Finite virtualized lists no longer lose rows that appear only between the
  first and final rendered windows during scrolling.

- `load_more` guidance now refers only to content controls. Generic “More”
  controls in site navigation, headers, footers, menus, and dialogs are no
  longer offered or clicked.

- Complete static pages no longer report incomplete feed coverage. A
  `windowed_feed` disclosure now means the browser restored content that the
  page removed during scrolling.

- Pagination guidance now requires credible evidence that the current
  collection continues. Unrelated `page=1` links and carousel slide counts no
  longer produce whole-page pagination claims.

- The built-in browser now captures complete collections when a page
  initially shows only a preview and reveals the rest through an in-page
  control.

- Extraction quality improved on complex multi-section pages, with more
  complete record coverage and more accurate Section boundaries.

- Comparison results now keep their row and column identities together.
  Authored blank values are returned as `null`, visual-only values are named
  in `withheld_columns`, and comparisons that cannot be preserved as records
  point to markdown instead. Fit guidance recommends structured comparison
  only when both axes are named and no comparison fields are withheld. Wire
  schema 22.

- Transient gateway failures no longer spend daily quota more than once
  for one extraction. Retries share the logical extraction's quota
  accounting without retaining the URL, page content, or result
  server-side.

- Pages with a continuously-updating DOM (live clocks, ad slots,
  client-side hydration) now capture much faster in the built-in
  browser. Such a page never sits perfectly still, so the pre-extraction
  scroll pass used to run to its internal cap before giving up; it now
  recognizes a settled page promptly and captures the same content in a
  fraction of the time.

- The first help screen (`analog --help`) now shows the recommended
  one-call Python usage — `from analog import analog;
  analog("https://example.com")` — instead of the lower-level hosted
  client.

## 0.15.0 — 2026-07-18
### Added

- **`pages=N` now captures script-driven pagination.** In-page
  batches are merged under the same result contract as link-followed
  pages. `page_sweep.mechanism` is `"in_page"`,
  `batches_captured` reports how many batches arrived, and
  `pagination.in_page_controls` discloses the available controls. If
  a control navigates away, captured batches are kept and the stop
  reason is `control_navigated`. See the
  [fetching guide](https://getanalog.io/docs/fetching/).

- **`pagination.stated_total` reports the page's own collection
  count.** It is the site's claim, not Analog's record count, so both
  remain visible when they differ.

- **Sticky field orders.** `analog reorder-fields <ref> FIELD...`
  and `analog.reorder_fields()` put named fields first on a saved
  result and remember the order for later fetches of the same URL.
  `--once` or `sticky=False` applies it only to the current result;
  replayed rules appear in `applied_orders`. See
  [work with results](https://getanalog.io/docs/results/).

- **Comparison tables arrive as one joined section.** Compared
  plans or models are records keyed by `table_column`; grouped table
  fields expose their heading through `FieldInfo.table_group`. Wire
  schema 17.

- **`assess` reports `navigation_probe`.** The fit check can show
  top-level destinations from the already-fetched page before an
  extraction is spent. See the
  [fit-check guide](https://getanalog.io/docs/assess/).

- **`section.withheld_visual` identifies glyph-only columns.** The
  disclosure names the columns and directs the reader to a clipped
  screenshot rather than guessing what a glyph means. Wire schema 16.

- **Results now carry the page's navigation and outline.**
  `result.navigation` contains labeled link trees;
  `result.outline` distinguishes extracted, non-extracted, and
  structural regions and replaces `page_sections`. Both appear in
  `preview()` and are searchable with `find()`.

- **Sections expose `Section.label_url`.** When a section heading
  has one unambiguous destination, the absolute URL points to the
  fuller page; otherwise it remains empty.

- **Greenhouse fit checks disclose the board's public jobs feed.**
  The feed's own current total is reported as `result.fit.item_count`,
  the provider-neutral replacement for `product_count`.

### Changed

- Uniform multi-page sweeps use less service compute while preserving
  the same results. Wire schema 19.

- **Outline outcomes are more specific.** Empty repeated regions use
  `no_fields_detected` or `no_emittable_fields`; overlapping
  regions use `overlapping_area_extracted` and may point to the
  shipped section through `superseded_by`. The catch-all
  `not_selected` is retired. Wire schemas 18, 20, and 21.

- **The feedback vocabulary gains `garbled_markdown`** for text that
  returned malformed or out of order.

- Extraction quality improved on script-driven listings, comparison
  tables, navigation-heavy pages, and table-heavy pages.

### Fixed

- `--load-all` reaches a wider range of clearly labeled load-more
  controls.

- Bot-protection challenges no longer show pagination or fit advice
  derived from the challenge page.

- `assess` preserves the page's most prominent navigation
  destinations when a menu contains many links.

- Automatic browser interactions no longer replace the requested page
  with a navigated destination. If the captured address still changes,
  the result discloses both addresses.

- `analog find` searches navigation and outline entries as well as
  records. `--field` continues to restrict the search to records.

- `export --fields` now rejects unknown fields with the available
  field names and guidance for heterogeneous sections.

## 0.14.0 — 2026-07-15
### Added

- **`analog docs`, `analog contact`.** Pointer commands: `docs` prints
  the documentation URL (pure output — `analog get $(analog docs)
  --mode local` reads the docs in the terminal); `contact` prints the
  team email and points at `analog feedback` for reports that go
  straight into triage.

### Changed

- **Analog fetches only public web addresses by default.** `get` and
  `assess` now decline local and private-network hosts and non-web
  schemes like `file:`, so a page can't steer a fetch at `localhost`,
  an internal address, or the local disk. Pass `--allow-private`
  (`allow_private=True`) to reach a dev server or intranet host on
  purpose. Paginated sweeps follow only links on the page's own host.

### Fixed

- **Signed-out `analog get` says what it's doing.** Without a sign-in,
  `get` returns the page's markdown (`mode=local`) — it now says so on
  stderr and names the fix: run `analog login` for structured records.
  Previously the downgrade was silent, and a first run before `login`
  could read as "Analog returns prose."
- **Numeric columns survive renames and narrowed exports.** Renaming a
  numeric field (including swaps) keeps its normalized numbers under
  the new name, and `export --envelope` re-aligns per-record numbers
  and structural-outlier positions after `--where`/`--sort`/`--limit`/
  `--fields`.
- **Detail toggles inside a card's own header now expand.** With
  `--expand-all` (`expand_all=True`), per-item disclosure toggles that
  sit inside a section's header are recognized and opened, so their
  detail is captured — previously only toggles outside a header were.
- **A deactivated account ends with a clear terminal error** — the
  commands name the state and stop, instead of a generic sign-in
  failure.
- **Hardened the fetch and sign-in paths for launch.** The site-feed
  probe streams and size-caps what it downloads, so a hostile server
  can't balloon memory; and sign-in, credential handling, and result
  parsing got a robustness pass.

## 0.13.0 — 2026-07-11

### Added

- **`analog help` — the subcommand form of `--help`.** `analog help`
  prints the top-level help; `analog help get` prints exactly what
  `analog get --help` prints; nested paths work (`analog help
  feedback quality`). `-h` now works everywhere as the short form.

### Changed

- **The feedback label vocabulary is now version 3** — every label
  names what you observed in the result (`no_records_found`,
  `misfielded_values`, `page_chrome_in_records`,
  `wrong_content_extracted`, …). At open launch the vocabulary
  freezes to additive-only.
- **Wire schema is now 13.** `Section.items_detected` counts the
  repeating items the page shows in the section's area. Results
  saved by earlier versions reopen unchanged.

## 0.12.1 — 2026-07-10

Paginated collections can be swept —
`analog(url, pages=N)` (CLI: `--pages N`) follows the site's own next
links, politely, capped at 50, and discloses its coverage on
`result.page_sweep`. Per-item detail toggles can be expanded
(`expand_all=True` / `--expand-all`), and long feeds no longer lose
records to scrolling. Also: saved results remember how they were
fetched; virtualized tables disclose the page's own declared size;
`result.rate_limit` gains the daily quota; the package ships
`py.typed`; `analog assess` mentions a site's sitemap when robots.txt
lists one; back-to-back extractions got dramatically faster (one warm
browser per process); legacy-encoded pages decode correctly on the
plain-GET path; and a long list of contract, disclosure, and
robustness fixes landed alongside. Extraction quality improved on
discussion threads (records gained a `depth` field), menu-styled
pages, and multi-paragraph content.

## 0.12.0 — 2026-07-06

`analog feedback` — quality reports with standardized labels, and
feature requests. Extraction URLs are never logged; a quality report
includes the URL you explicitly submit and never page content.
`result.to_dataframe()`; filtered history deletion (`analog rm
--older-than 30d`); store disk-usage reporting; partial-coverage
disclosures for paginated pages; the credential moved from the OS
keychain to a local owner-only file (run `analog login` once after
upgrading); a lighter install; and the fit check reworked around
measured facts and task guidance. Extraction quality improved on
store and listing pages, and field-naming contracts firmed up —
`price` is always the pay-now price, with `price_excl_vat` and
`review_count` beside it where pages show them.

## 0.11.0 — 2026-07-04

`result.records`; `analog find`; section-addressable markdown;
field renaming as a library operation; guessed-name and
page-truncation disclosures; withheld-column warnings; documented
exit codes on every command. Extraction quality improved on complex
HTML tables.