{
  "schemaVersion": 1,
  "sdk": "sdkpy",
  "baseline": {
    "version": "0.14.2",
    "markdown": "# Changelog — `warmhub`\n\n## 0.14.2\n\nRepository export v3.\n\n### Breaking\n\n#### Checkpoint generation is retired — take an export instead\n\n**What it was.** Producing a fresh snapshot of a repository meant asking the\nserver to mint a checkpoint, polling `status` until the job completed,\nrequesting a short-lived signed URL, downloading the archive outside the SDK,\nand verifying the bytes locally.\n\n**What it is now.** One call that streams verified rows. Framing, canonical\ndecode, per-row schema, ordering and the trailer are checked as the bytes\narrive, and a response that dies mid-body is re-requested from the last\nverified row into the *same* reader — so a stream that arrived in four pieces\nstill verifies against one digest and one record count.\n\n**Migration.**\n\nBefore:\n\n```python\nimport time\n\nimport httpx\n\nfrom warmhub import verify_repository_checkpoint_archive\n\nstatus = client.repo.checkpoint.generate(\"acme\", \"catalog\")\nwhile status.state != \"complete\":\n    time.sleep(1)\n    status = client.repo.checkpoint.status(\n        \"acme\", \"catalog\", checkpoint_id=status.checkpoint_id\n    )\n\naccess = client.repo.checkpoint.get_access(\n    \"acme\", \"catalog\", checkpoint=\"latest\", artifact=\"archive\"\n)\n# Signed URL: fetched WITHOUT the WarmHub bearer token.\nwith open(\"catalog.zip\", \"wb\") as handle:\n    handle.write(httpx.get(access.url).content)\nverify_repository_checkpoint_archive(\"catalog.zip\")\n```\n\nAfter:\n\n```python\nfrom warmhub import open_repository_export\n\nexport = open_repository_export(client, \"acme\", \"catalog\")\nfor row in export.rows:\n    print(row[\"kind\"], row[\"durableId\"], row[\"version\"])\n\n# `reader.trailer` is None until `rows` is fully consumed: the export is\n# verified at that moment, not before.\nprint(export.reader.record_count, export.reader.content_sha256)\n```\n\nIf you were relying on `generate` returning before the download — starting the\nbuild in one process and fetching it in another, or on another machine — that\nshape survives as the token flow:\n\n```python\nimport time\n\nfrom warmhub import (\n    RepositoryExportPending,\n    open_repository_export_token,\n    request_repository_export,\n)\n\n# Process A: ask for it, get a ticket, do not read a byte.\nticket = request_repository_export(client, \"acme\", \"catalog\")\nprint(ticket.export_token, ticket.at_repo_seq)\n\n# Process B: redeem it. A redemption answered 202 comes back as\n# RepositoryExportPending; poll it yourself.\nwhile True:\n    opened = open_repository_export_token(\n        client, \"acme\", \"catalog\", ticket.export_token\n    )\n    if not isinstance(opened, RepositoryExportPending):\n        break\n    print(\"still\", opened.state)  # \"pending\" or \"running\"\n    time.sleep(1)\n\nfor row in opened.rows:\n    ...\n```\n\nThe token is redeemable only at that repository's URL, by a principal holding\ncheckpoint read there.\n\n**Why.** Export format v3 replaces checkpoint generation outright; the whole\ngeneration pipeline is deleted server-side (PR #10020).\n\n#### `repo.checkpoint.generate` and `.retry` are gone\n\n**What it was.** `generate(org_name, repo_name, at_least_repo_seq=...)` minted\na checkpoint and `retry(org_name, repo_name, checkpoint_id)` re-ran a failed\none, on the unbound namespace and on the repository-bound one, in both the sync\nand async clients. The payload builders\n`checkpoint_generate_payload` and `checkpoint_retry_payload` are gone from\n`warmhub.namespaces.repo_checkpoint` with them.\n\n**What it is now.** `repo.checkpoint` is a read plane: `status`, `latest`,\n`get_access`. The procedures behind `generate` and `retry` are no longer served,\nand the `repo:checkpoint-generate` scope no longer buys anything.\n\n**Migration.**\n\nBefore:\n\n```python\nfresh = client.repo.checkpoint.generate(\"acme\", \"catalog\", at_least_repo_seq=42)\nrecovered = client.repository(\"acme/catalog\").repo.checkpoint.retry(\n    failed.checkpoint_id\n)\n```\n\nAfter:\n\n```python\nfrom warmhub import open_repository_export\n\n# `at_repo_seq` pins the fence the way `at_least_repo_seq` asked for a floor.\n# Omit it and the server pins current and echoes it back in the header.\nexport = open_repository_export(client, \"acme\", \"catalog\", at_repo_seq=42)\nfor row in export.rows:\n    ...\n```\n\nThere is no retry to replace: an export is a read, so a failed one is re-read,\nnot re-queued — and the reader resumes a broken transport on its own, up to\n`resume_limit` (4 by default).\n\n**Why.** Same retirement (PR #10020).\n\n#### Failed checkpoint statuses now all answer `next_action=\"none\"`\n\n**What it was.** `RepositoryCheckpointFailedDeadlineExceeded` and\n`RepositoryCheckpointFailedAttemptsExhausted` carried\n`next_action=\"retry\"`; `RepositoryCheckpointFailedInvalidSource` carried\n`next_action=\"generate\"`. Each named a method a reader could call.\n\n**What it is now.** All three carry `next_action=\"none\"`. The dataclasses keep\ntheir names and their `failure_code` values, so pattern-matching on\n`failure_code` is unaffected; only branching on `next_action` changes.\n\n**Migration.**\n\nBefore:\n\n```python\nif status.state == \"failed\":\n    if status.next_action == \"retry\":\n        client.repo.checkpoint.retry(\"acme\", \"catalog\", status.checkpoint_id)\n    elif status.next_action == \"generate\":\n        client.repo.checkpoint.generate(\"acme\", \"catalog\")\n    elif status.next_action == \"contact_support\":\n        report(status.checkpoint_id, status.failure_code)\n```\n\nAfter:\n\n```python\nif status.state == \"failed\":\n    if status.next_action == \"contact_support\":\n        report(status.checkpoint_id, status.failure_code)\n    else:\n        # Terminal. Take an export instead of reviving the checkpoint.\n        export = open_repository_export(client, \"acme\", \"catalog\")\n        for row in export.rows:\n            ...\n```\n\n**Why.** Each retired value pointed at a route the server no longer serves;\nleaving them in the vocabulary would send readers to a 404 (PR #10020).\n\n### Added\n\n- **`open_repository_export(client, org_name, repo_name, *, mode=\"heads\",\n  since_repo_seq=0, at_repo_seq=UNSET, resume_limit=4)`** — returns a\n  `RepositoryExportStream` with `.reader` and `.rows`. `aopen_repository_export`\n  is the `async` twin, returning `AsyncRepositoryExportStream` whose `.rows` is\n  an `async` iterator.\n\n- **`request_repository_export(...)` / `arequest_repository_export(...)`** —\n  ask the server to build the export in the background. Returns a\n  `RepositoryExportTicket` with `export_token` and `at_repo_seq`.\n\n- **`open_repository_export_token(client, org_name, repo_name, export_token, *,\n  resume_limit=4)`** and its `aopen_...` twin — redeem a ticket. Returns either\n  the stream or a `RepositoryExportPending` whose `state` is `\"pending\"` or\n  `\"running\"`; the first redemption does not poll for you.\n\n- **`RepositoryExportReader`** — the verification state of one logical export\n  across one or more transport segments. Read `header`, `trailer`,\n  `record_count`, `content_sha256`, `after_durable_id`, and `is_complete`. It\n  is also usable directly on bytes you obtained yourself, via\n  `read_segment(chunks)` / `feed(chunk)` / `end_segment()` / `finish()`.\n\n- **`verify_repository_export_file(source)`** — re-verify a saved export end to\n  end with no network access. Accepts `bytes`, `bytearray`, or a path, and\n  returns a `RepositoryExportVerification` with `header`, `trailer`, and\n  `record_count`.\n\n  ```python\n  from warmhub import verify_repository_export_file\n\n  result = verify_repository_export_file(\"catalog.ndjson\")\n  print(result.header.at_repo_seq, result.record_count)\n  ```\n\n- **`apply_repository_export_delta(base, delta)`** — fold a delta export over a\n  base by durable identity: active rows upsert, tombstones drop. Pure and\n  order-preserving — rows already present keep their position, new rows land at\n  the end — and it returns a `list`, so folds chain.\n\n  ```python\n  from warmhub import apply_repository_export_delta, open_repository_export\n\n  full = open_repository_export(client, \"acme\", \"catalog\")\n  base = list(full.rows)\n  fence = full.reader.header.at_repo_seq\n\n  # Later: only what changed since that fence.\n  changed = open_repository_export(client, \"acme\", \"catalog\", since_repo_seq=fence)\n  current = apply_repository_export_delta(base, list(changed.rows))\n  ```\n\n  Durable identity is the key rather than `wref`, because a rename changes the\n  wref and this fold must survive one.\n\n- **`RepositoryExportError`** with a typed `reason`: `\"count_mismatch\"`,\n  `\"digest_mismatch\"`, `\"non_canonical_line\"`, `\"record_invalid\"`,\n  `\"request_failed\"`, `\"row_order\"`, `\"segment_mismatch\"`,\n  `\"since_below_epoch_floor\"`, `\"stream_closed\"`, `\"truncated_stream\"`,\n  `\"unexpected_response\"`. `\"since_below_epoch_floor\"` is the only one that is\n  not a fault: the requested delta base is older than the server retains, and\n  the recovery is to re-run the export with `since_repo_seq=0`.\n\n- **`RepositoryExportHeader`, `RepositoryExportTrailer`,\n  `RepositoryExportRow`, `RepositoryExportMode`,\n  `RepositoryExportVerification`** — the record types. A row is a validated\n  mapping, not a dataclass: `data` holds your own Shape fields, so decoding it\n  into a frozen class would only be something you had to undo. The envelope\n  keys — `kind`, `wref`, `durableId`, `version`, `active` — are guaranteed\n  present.\n\n- **`stream=True` on `SyncTransport.send` / `AsyncTransport.send`** — returns\n  before the body is read, for responses whose whole point is not to be\n  buffered. The caller owns closing the response.\n\n### Behavior worth knowing\n\n- **A broken transport recovers itself, differently on the two paths.** A\n  direct stream that ends short, or dies mid-body, is re-requested from\n  `after_durable_id` into the same reader; the partial trailing line is never\n  fed, because the next `begin_segment` drops it. A redeemed token has no\n  cursor — its bytes come from storage rather than the API — so a dead transfer\n  fetches the object again from the top and the reader restarts with it. Either\n  way you are never handed the same row twice. Both are bounded by\n  `resume_limit` (default 4) and raise `truncated_stream` when it runs out — a\n  server that keeps cutting the stream should surface, not spin.\n\n- **A redeemed token is downloaded from storage, then held to what the API\n  said.** A complete redemption answers with a short-lived presigned URL rather\n  than the export bytes, and the SDK fetches that URL *without* your WarmHub\n  bearer: the signature in the URL is the whole capability, and the storage host\n  has no business seeing a credential. The record count and content digest the\n  reader derives are then checked against the ones the API named, because the\n  trailer inside the object cannot vouch for the object — a download that does\n  not match raises `count_mismatch` or `digest_mismatch` rather than handing you\n  rows nothing independent vouched for. An expired URL re-redeems the token,\n  which is a deterministic lookup, and retries.\n\n- **Ordering is verified, in `heads` mode only.** A heads export is a set\n  emitted in ascending durable-identity order, and that order is part of what\n  the digest proves; a row that does not advance the cursor raises\n  `row_order`. An `ops` export is a log and repeats identities by design.\n\n- **A trailer describes its segment, not the export.** `RepositoryExportTrailer`\n  carries the `record_count` and `content_sha256` of the rows in the segment it\n  closed — the whole export only when it arrived in one piece.\n  `reader.record_count` and `reader.content_sha256` are the whole-stream values.\n\n- **`202` means different things on different paths.** On a token redemption it\n  is the job still building, and comes back as `RepositoryExportPending`. On a\n  direct or resumed stream request it is `unexpected_response` — there, the\n  stream itself was supposed to be the whole answer.\n\n- **Verification finishes with the rows.** `reader.trailer` is `None` until\n  `rows` has been fully consumed. Abandoning the iterator mid-way leaves the\n  export unverified; do not treat partially-read rows as proven.\n\n### Still supported\n\nThe checkpoint **read** plane is untouched: `repo.checkpoint.status`,\n`.latest`, `.get_access`, and `verify_repository_checkpoint_archive` all continue\nto work against archives already in object storage. Nothing mints new ones.\n\n**Deprecation horizon:** those read surfaces are supported until the announced\ncleanup, 30 days after GA. Their removal is a separately-filed post-GA change.\nMove download-and-verify workflows to the export surface before then.\n"
  },
  "releases": [
    {
      "version": "0.14.3",
      "sourceSha": "4ab1a8b0c92c8852e3ed2bbfbe1ef17ed0f79dc6",
      "notes": [
        {
          "id": "backfill-sdkpy-0-14-3",
          "body": "### Release changes\n\n- chore(sdk): regenerate the contract artifacts for the count result's hasMore (commit `414aefde7369`)\n\nReconstructed from SDK release tags."
        }
      ]
    },
    {
      "version": "0.14.4",
      "sourceSha": "5e3bfb7a6e3024eb9e16707695d3e25a7e01bbf1",
      "notes": [
        {
          "id": "backfill-sdkpy-0-14-4",
          "body": "### Release changes\n\n- chore(compat): #10974 retire transitional client contracts and startup adoption (commit `4347a0500db6`)\n\nReconstructed from SDK release tags."
        }
      ]
    },
    {
      "version": "0.15.0",
      "sourceSha": "29f70ba169ad15b09f72b7a07782d9662a7d1191",
      "notes": [
        {
          "id": "backfill-sdkpy-0-15-0",
          "body": "### Release changes\n\n- feat(collections): Arc and Bond become ordinary Things under protected V2 Shapes (commit `50fec9ce2a8f`)\n- fix(collections): suppress the migrated Arc THING on replay and reserve Collection in Python (commit `171e5daabba7`)\n- feat(collections): the Collections V2 cutover with a legacy-sugar window (commit `adaaa4fe53dc`)\n- feat(collections): classify inbound traversal by closure and add the aggregate roles (commit `ca07faa123f0`)\n- feat(collections): make the collection router read the certificate closure (commit `74bf13faaf25`)\n- feat(collections): split the read kind and carry collections in the Python SDK (commit `cae90e67bf42`)\n- fix(collections): carry the summary rename into whedge and the e2e-py suites (commit `dc266e612abb`)\n- fix(collections): stop head and history claiming member facts they cannot see (commit `9c6752da5e8d`)\n- fix(sdk-py): decode collections on paged rows, not just on detail reads (commit `f7bbfcab6364`)\n- feat(collections): put the member layout on the Shape and the facts on the row (commit `be4d60df9499`)\n- test(collections): pin the layout ruling and the four review findings it touches (commit `102b6e20d7a2`)\n- fix(collections): finish the summary reshape in the two packages CI typechecks separately (commit `338faa58a7ea`)\n- chore(sdk): regenerate the conformance artifacts over main's retired contracts (commit `43e4ee9ef5c8`)\n- feat(export)!: carry Shape certificates as pinned wref strings (commit `30187cb8082e`)\n- fix(collections)!: name a foreign certifying Shape by its repository (commit `18478e45b47f`)\n\nReconstructed from SDK release tags."
        }
      ]
    },
    {
      "version": "0.15.1",
      "sourceSha": "f990f080d6f8b7310700b1d430ee1aa11dcaaf23",
      "notes": [
        {
          "id": "published-changelog-artifacts",
          "body": "### Documentation\n\nVersioned release notes are available as [HTML, Markdown, and JSON](https://docs.warmhub.ai/sdk/python/changelog/)."
        }
      ]
    },
    {
      "version": "0.16.0",
      "sourceSha": "e3b82b4b08936c141a727fc4f072a5d9ee1eeb31",
      "notes": []
    },
    {
      "version": "0.16.1",
      "sourceSha": "756a13ed78b5ffc90a2640f8b9b0a1db9233f6a4",
      "notes": []
    },
    {
      "version": "0.17.0",
      "sourceSha": "eeb99f940dc369a637a9b63d86b3cb3fe9530fd7",
      "notes": []
    }
  ]
}
