Skip to content

Changelog

Release notes for analog-sdk.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • Added analog quickstart, a network-free guide to connecting an account and making a first extraction from the shell or Python.
  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.
  • 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.”

  • 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.
  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.

  • --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.

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.

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.

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.

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.