# Reliability & Accuracy

> **Status:** v0.1 — first pass, finalized 2026-05-15
> (review-pass verified all file:line citations against the
> live source). Last updated 2026-05-15.
> A federation-sibling-scoped reliability audit: walked the three
> source files (`_client.py`, `_retry.py`, `_errors.py`) end-to-end
> against the **RA** / **EH** / **SC** rule set from
> `mgf-standard/docs/standards/`, verified every claim against
> `file:line`. The sibling's public surface is three names
> (`AsyncHttpClient`, `RetryPolicy`, `HttpTransportError`) — narrow
> but sits on the outbound request path of every consumer.
>
> **Scope:** v1.0 readiness for mgf-http.
> **Audience:** the maintainer + future AI agents doing
> reliability work on this sibling.
> **Cross-references:** [`PUBLIC_API.md`](../../PUBLIC_API.md),
> [`docs/standards/README.md`](../standards/README.md),
> [`mgf-common/docs/inprogress/RELIABILITY_AND_ACCURACY.md`](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/docs/inprogress/RELIABILITY_AND_ACCURACY.md)
> (cornerstone).

This document is the **living priority tracker** for reliability
+ accuracy work in `mgf-http`. Mirrors mgf-common's tracker
shape.

The sibling is at v0.2.0 (post-v2.6/v2.7 compliance bump from
v0.1.1). Every outbound HTTP call from a consumer of this library
goes through `AsyncHttpClient`; reliability gaps here cascade to
every downstream API integration.

---

## §1 You are here

mgf-http ships three public names; `mypy --strict` clean; ruff
clean; the EH-11 retry-with-full-jitter pattern + the EH-12
idempotency-method boundary are wired correctly out of the
box; secret-shaped headers redact in log lines; TLS
verification is default-on with a one-shot warning when
disabled.

What this tracker exposes is the **edge-case shape** —
boundaries the happy path doesn't exercise but production
will: thread-racy module state (the one-shot warning),
provider-supplied retry hints (`Retry-After`), cookie values
with embedded control characters, the `follow_redirects=True`
default's open-redirect class for user-supplied URLs, and
the contents of `cause_summary` strings that propagate to
log + error messages.

**Counts (2026-05-17):**

| Priority | Open | Closed | Total |
|---|---|---|---|
| P0 — v1.0 surface-lock blockers | 0 | 0 | 0 |
| P1 — should land before/with v1.0 | 0 | 3 | 3 |
| P2 — post-v1.0 hardening | 0 | 3 | 3 |
| **Total** | **0** | **6** | **6** |

**Do first:** P2 sweep closed (RA-04..RA-06, 2026-05-17). All
v1.0-blocking reliability items now closed.

---

## §2 Priority table

| ID | Concern | One-line | Effort | Status |
|---|---|---|---|---|
| [**RA-01**](#ra-01-tls-disable-warning-race) | Thread-safety on global state | `_TLS_DISABLE_WARNED` is a module-level bool gated by a non-atomic test-and-set; two threads constructing `AsyncHttpClient(verify_tls=False)` simultaneously can both fire the warning (harmless) — but the pattern is wrong and may bite a future use case (e.g., a "loud-on-first" pattern that depends on single-firing). | XS | ✅ closed |
| [**RA-02**](#ra-02-429-retry-after-not-honoured) | Compliance with provider rate-limit hints | `_send_with_retries` retries 429 responses with jittered exponential backoff regardless of the `Retry-After` header. Providers (Stripe, GitHub, Cloudflare, Clerk) often send `Retry-After: 30` — the current policy ignores it and may exhaust the rate-limit window faster than the provider's intended schedule. | S | ✅ closed |
| [**RA-03**](#ra-03-cookie-value-sanitization) | Defensive input | Per-call `cookies=` values are interpolated into a single `Cookie:` header via `"; ".join(...)`. A cookie value with embedded `\r\n` would pass through; httpx may or may not reject (depends on h11/h2 version). Validate upstream. | XS | ✅ closed |
| [**RA-04**](#ra-04-cause-summary-leaks-httpx-exception-text) | Information disclosure | `_categorise_exc` interpolates `str(exc)` from httpx's exception into `cause_summary`. httpx's str repr typically doesn't include sensitive data, but a future httpx version or a specific exception subclass could surface URLs / headers / body fragments. The string ends up in `HttpTransportError`'s message + consumer logs. | S | ✅ closed |
| [**RA-05**](#ra-05-follow-redirects-true-default-open-redirect-class) | Documented carve-out | `httpx.AsyncClient(..., follow_redirects=True)` is the default. For consumer-supplied URLs (where the URL itself comes from user input — rare but happens), an open-redirect attack could route a request to an unexpected host. Today this is a "documented limitation" (the library is for outbound API-to-provider calls); make the limitation explicit and add an opt-out path. | XS | ✅ closed |
| [**RA-06**](#ra-06-aclose-idempotency-pin) | Test coverage for documented invariant | `AsyncHttpClient.aclose()`'s docstring says "Safe to call multiple times" — httpx's underlying `aclose()` is idempotent, so this propagates. But no test pins the contract. A future httpx change that breaks idempotency would silently regress. | XS | ✅ closed |

Effort scale: XS (≤ 15 min); S (≤ 1 h); M (≤ 1 session); L
(multiple sessions).

---

## §3 Invariants we hold

The list of "closed elsewhere; do not re-open":

- **RA-INV-1** — TLS verification is **default-on**.
  `AsyncHttpClient.__init__` accepts `verify_tls: bool = True`
  ([`_client.py:94`](../../src/mgf/http/_client.py#L94)); the
  underlying `httpx.AsyncClient(verify=verify_tls, ...)` honours
  it. Disabling emits `UserWarning` (single-shot per process,
  per RA-01 below).
- **RA-INV-2** — POST is **excluded** from default
  `retryable_methods`. `_DEFAULT_RETRYABLE_METHODS = {"GET",
  "HEAD", "OPTIONS", "PUT", "DELETE"}`
  ([`_retry.py:30`](../../src/mgf/http/_retry.py#L30)).
  Per **EH-12** idempotency rule.
- **RA-INV-3** — Backoff uses **full jitter**.
  `backoff_for_attempt` returns
  `random.random() * min(base * 2^attempt, max_backoff)`
  ([`_retry.py:95`](../../src/mgf/http/_retry.py#L95)).
  Per **EH-11**; matches the AWS Architecture blog's empirical
  finding.
- **RA-INV-4** — Secret-shaped request headers are **redacted**
  in log lines. The redaction set covers
  `Authorization` / `Cookie` / `Set-Cookie` / `X-Api-Key` /
  `X-Auth-Token` / `X-Secret` / `Proxy-Authorization`
  ([`_client.py:50-61`](../../src/mgf/http/_client.py#L50)).
  Per **LG-04** + **SC-09**.
- **RA-INV-5** — User-Agent identifies the **calling app +
  the library**. `_default_user_agent` returns
  `<app_name>/<app_version> mgf-http/<libver>`
  ([`_client.py:381`](../../src/mgf/http/_client.py#L381)).
  Upstream logs can attribute traffic.
- **RA-INV-6** — `HttpTransportError` preserves the original
  httpx exception as `__cause__`
  ([`_client.py:341 + :369`](../../src/mgf/http/_client.py#L341)).
  Per **EH-03**; debugging chain stays intact.
- **RA-INV-7** — Per-call `timeout=` overrides the constructor
  default ([`_client.py:226`](../../src/mgf/http/_client.py#L226)).
  Per the documented kwarg shape.
- **RA-INV-8** — Backoff sleep is `await asyncio.sleep(backoff)`
  ([`_client.py:274`](../../src/mgf/http/_client.py#L274)).
  Cancellation propagates correctly (no try/except around it).
  Per **EH-04** + the cancellation re-raise rule in
  ERROR_HANDLING.md §11.5.

---

## §4 Work units

### RA-01 TLS-disable warning race

**Why:** Module-level `_TLS_DISABLE_WARNED` boolean
([`_client.py:65`](../../src/mgf/http/_client.py#L65)):

```python
_TLS_DISABLE_WARNED = False

class AsyncHttpClient:
    def __init__(self, *, verify_tls: bool = True, ...):
        if not verify_tls:
            global _TLS_DISABLE_WARNED
            if not _TLS_DISABLE_WARNED:
                _TLS_DISABLE_WARNED = True
                warnings.warn(...)
```

The test-and-set pattern is non-atomic. Two threads both
constructing `AsyncHttpClient(verify_tls=False)` at the same
time (race conditions in pytest-xdist test setup, an app
that builds multiple clients concurrently) can both read
`_TLS_DISABLE_WARNED is False` before either sets it →
both fire the warning.

The harm today is minimal — an extra `UserWarning` line.
But the pattern is wrong and:

- Sets a precedent for non-atomic module-state mutation
  that future code might copy with worse consequences.
- A future "loud one-shot diagnostic" use case (e.g., "warn
  on first downgraded TLS handshake to deprecated TLS 1.2")
  that uses the same shape would fire multiple times in
  parallel test runs.

**Concrete output:**

- Replace `_TLS_DISABLE_WARNED` + the test-and-set with a
  `threading.Lock`-guarded version, OR replace the bool with
  `itertools.count()` + a `next() == 0` check (atomic), OR
  use `warnings.filterwarnings("default", category=...)` to
  let the warnings module dedupe.
- Pick whichever is cleanest. The `threading.Lock` shape is
  the safest + most readable.
- Add a test in `tests/unit/test_client_tls_warn.py` that
  fires N threads constructing `AsyncHttpClient(verify_tls=False)`
  simultaneously and asserts exactly one warning is emitted.

**Locked in by:** the new test fails on first run if the
race regresses.

**Cross-references:** RA-INV-1.

---

### RA-02 429 `Retry-After` not honoured

**Why:** `_send_with_retries` retries on status
([`_client.py:347-360`](../../src/mgf/http/_client.py#L347)):

```python
if (
    attempt < max_attempts
    and retry_policy.is_status_retryable(response.status_code)
):
    ...
    await response.aclose()
    continue
```

The next iteration calls `retry_policy.backoff_for_attempt(...)`
which returns full-jitter exponential backoff. The `Retry-After`
header — RFC-7231 mandated for 429 + 503 + 504 responses —
is ignored.

Provider impact:

- **Stripe** sends `Retry-After` as integer seconds. Ignoring
  burns the rate-limit window with extra retries.
- **GitHub** sends `Retry-After` as integer seconds AND/OR an
  HTTP-date (depending on the rate-limit category).
- **Cloudflare** sends `Retry-After` plus `X-RateLimit-Reset`
  for context.
- **Clerk** sends `Retry-After` per the Svix-derived rate-limit
  shape.

Ignoring this is a real bug: a consumer hitting a rate limit
sends 3 retries on its own (jittered) schedule, may hit the
window before the server's `Retry-After` window opens, and
gets re-rate-limited.

**Concrete output:**

- In the retry-on-status branch, parse `response.headers.get("Retry-After")`:
  - If absent: fall through to existing backoff calc.
  - If a numeric string: `min(int(value), retry_policy.max_backoff_seconds)`.
  - If an HTTP-date string: convert to delta-seconds via
    `email.utils.parsedate_to_datetime`; same cap.
  - On parse failure: fall back to existing backoff calc + log
    a WARNING.
- Add `RetryPolicy.honour_retry_after: bool = True` flag so
  consumers can opt out (rare; tests against a misbehaving
  provider).
- Add unit tests covering the three shapes (absent, numeric,
  HTTP-date) + parse-failure fallback.

**Locked in by:** the new tests pin the parse logic across
the three shapes; a regression that drops the Retry-After
read fails them.

**Cross-references:** RFC 7231 §7.1.3; **EH-11** retry-with-
backoff rule (which the current implementation satisfies but
doesn't fully realise — Retry-After is the canonical hint).

---

### RA-03 Cookie value sanitization

**Why:** `_send_with_retries` builds the `Cookie:` header
([`_client.py:282-292`](../../src/mgf/http/_client.py#L282)):

```python
if cookies:
    cookie_header = "; ".join(f"{k}={v}" for k, v in cookies.items())
    ...
    request_headers["Cookie"] = cookie_header
```

A consumer passing `cookies={"sess": "abc\r\nX-Inject: yes"}`
gets that value interpolated into a header value. Whether the
downstream stack rejects it depends on the httpx version's
underlying h11/h2 strictness:

- httpx ≥ 0.27 with h11 ≥ 0.14 enforces RFC 7230 header
  validation → rejects with `LocalProtocolError`.
- Older versions or alternate transports MAY pass it through.

Per defence-in-depth: validate at the BOUNDARY (here, where
we accept user-supplied cookie dicts), not just at the
transport. Today the wrapper trusts the caller; a robust
shape catches the bad value before the transport even sees
it.

**Concrete output:**

- A `_validate_cookie_value(value: str) -> None` helper that
  rejects values containing `\r`, `\n`, `\0`, or `;`.
- Call it for each value in `cookies.items()` before joining.
- On rejection: raise `ValueError("cookie value contains invalid characters")`
  with a clear message.
- Add unit tests:
  - Valid cookies pass through.
  - `cookies={"k": "v\r\nbad"}` raises `ValueError`.
  - `cookies={"k": "v\twith tab"}` is accepted (tabs are
    allowed in cookie values per RFC 6265 — but check
    httpx's stricter rejection here too; if httpx rejects,
    we should too).

**Locked in by:** the unit test exercises the boundary; a
regression that drops the validation fails it.

**Cross-references:** RFC 6265 §4.1.1 (cookie-value
character set); SC-09 (sink-side redaction); RA-01 in
mgf-django (same input-validation discipline at a different
boundary).

---

### RA-04 `cause_summary` leaks httpx exception text

**Why:** `_categorise_exc`
([`_client.py:443-451`](../../src/mgf/http/_client.py#L443)):

```python
def _categorise_exc(exc: BaseException) -> str:
    if isinstance(exc, httpx.TimeoutException):
        return f"timeout: {exc}"
    if isinstance(exc, httpx.ConnectError):
        return f"connect error: {exc}"
    if isinstance(exc, httpx.NetworkError):
        return f"network error: {exc}"
    return f"{type(exc).__name__}: {exc}"
```

The `str(exc)` interpolation is the source of the leak risk.
httpx's exception `__str__` typically returns clean messages
like `"timeout"` or `"[Errno 111] Connection refused"` — but:

- A future httpx version (or a specific exception subclass we
  don't currently match) might include the request URL, the
  full ConnectError trace, or response body fragments in the
  str.
- The `cause_summary` ends up in
  `HttpTransportError`'s message (consumer logs, CLI output,
  potentially HTTP response bodies via consumer error
  handlers).

If a request URL contains a query-string-embedded credential
(`?api_key=...`), str(httpx.ConnectError(...)) MIGHT include
it; we don't validate that today.

**Concrete output:**

- A controlled summary string per known exception class —
  return the category name only (no `str(exc)` interpolation).
  e.g.,
  - `httpx.TimeoutException` → `"timeout"`
  - `httpx.ConnectError` → `"connect-error"`
  - `httpx.NetworkError` → `"network-error"`
  - Default: `type(exc).__name__` only (NO str(exc)).
- Move the detailed `str(exc)` to a SEPARATE structured field
  on `HttpTransportError` (`exc.cause_str: str | None`) that
  consumers can opt-in to log / show. Default-redacted from
  the user-facing message.
- The original `httpx` exception stays available as
  `__cause__` — debuggers and crash reporters that already
  scrub through the redactor will see + sanitize it via the
  `mgf-common` Redactor pass.

**Locked in by:** unit tests asserting `HttpTransportError`'s
`str()` for each exception class doesn't contain the URL
or the full httpx message.

**Cross-references:** SC-11 (traceback scrubbing); LG-04
(formatter redaction).

---

### RA-05 `follow_redirects=True` default — open-redirect carve-out

**Why:** `httpx.AsyncClient(..., follow_redirects=True)`
([`_client.py:131`](../../src/mgf/http/_client.py#L131)) — httpx
defaults to NOT following redirects; we explicitly enable it.
For outbound API-to-provider calls this is sensible (the
provider's redirect to a CDN is normal). For consumer-supplied
URLs (rare — most consumers hit a fixed provider) this is an
open-redirect class:

- User-supplied URL → 302 → attacker-controlled host →
  request follows + attaches cookies / Bearer.
- Particularly bad with `cookies=` carrying session tokens.

mgf-http's audience is outbound API integrations (the docstring
explicitly says so). But the carve-out should be EXPLICIT, not
just implicit-by-audience.

**Concrete output:**

- Add a `follow_redirects: bool = True` kwarg to
  `AsyncHttpClient.__init__` so consumers can opt out per
  client.
- Add a doc note in the constructor docstring:
  > Defaults to `True` because mgf-http's intended use case
  > is outbound API-to-provider calls where redirects are
  > normal (provider → CDN). For URLs that include
  > user-supplied components, set `follow_redirects=False`
  > or use a separate client with cookies / auth scoped
  > carefully.
- Update the recipe docs to call out this distinction.

**Locked in by:** the docstring + the new kwarg ARE the fix;
no regression test needed (the change is configuration
exposure, not behaviour change).

**Cross-references:** SC-09 (egress side of secret handling);
the recipe doc would benefit from a "common pitfall: don't
follow redirects on user-supplied URLs" note.

---

### RA-06 `aclose()` idempotency pin

**Why:** `AsyncHttpClient.aclose()`
([`_client.py:136-138`](../../src/mgf/http/_client.py#L136)):

```python
async def aclose(self) -> None:
    """Close the underlying connection pool. Safe to call multiple times."""
    await self._client.aclose()
```

The docstring claims "Safe to call multiple times" —
delegating to httpx's underlying `aclose()` (which IS
idempotent per httpx's contract). But:

- No test asserts the contract.
- A future httpx change to `aclose()` raising on second call
  would silently regress our wrapper's documented behaviour.
- A future refactor that adds wrapper-side state (e.g., "track
  whether close has been called") might forget to keep
  idempotency.

**Concrete output:**

- Add `tests/unit/test_client_aclose.py` with two tests:
  - `async with AsyncHttpClient() as c:` — implicit aclose
    once; assert no error.
  - `c = AsyncHttpClient(); await c.aclose(); await c.aclose()`
    — explicit double-close; assert no error.
- Optionally: add `tests/integration/test_aclose_double.py`
  that exercises against a real httpx version to catch
  upstream regressions on bump.

**Locked in by:** the unit tests pin the contract; a
regression breaks them.

**Cross-references:** DP-13 (deterministic resource release);
RA-INV-6 (the documented invariant this test enforces).

---

## §5 Closed items

- **RA-01** — TLS-disable warning thread-safety. Added a
  module-level `_TLS_DISABLE_WARN_LOCK = threading.Lock()` and
  wrapped the test-and-set of `_TLS_DISABLE_WARNED` inside it.
  Two threads racing on `AsyncHttpClient(verify_tls=False)`
  construction can no longer both observe the flag as False;
  exactly one fires the `UserWarning` regardless of timing.
  The harm pre-fix was minimal (duplicate warning lines), but
  the pattern was wrong and a future "warn-once" diagnostic
  added to this file would have inherited the same race. New
  unit test fires 20 threads concurrently and asserts exactly
  one warning under `catch_warnings(record=True)` +
  `simplefilter("always")`. (2026-05-17, commit pending.)
- **RA-02** — `Retry-After` honoured on retryable 4xx/5xx
  responses. Added `RetryPolicy.honour_retry_after: bool = True`
  (default-on; opt-out flag for testing against misbehaving
  providers). Added a module-level `_parse_retry_after(raw)`
  helper handling both RFC 7231 §7.1.3 shapes: numeric
  delta-seconds (Stripe, Clerk, Cloudflare common shape) and
  HTTP-date (GitHub on some rate-limit categories). The retry
  loop now captures the header value (capped at
  `max_backoff_seconds`) into a `next_retry_after` slot that
  takes precedence over the policy's jittered backoff for the
  upcoming iteration; cleared after one use so subsequent
  retries fall back to the policy. 7 new tests: 5 unit tests
  on the parser (numeric, negative, future date, past date,
  malformed) + 2 end-to-end tests pinning the override + the
  opt-out. (2026-05-17, commit pending.)
- **RA-03** — Closed-by-reference (SH-01). The cookie
  CR/LF/NUL guard shipped under SH-01 (commit `a7d75d3`); the
  defect is dual-framed (security + reliability) so the fix
  lands once. See SH-01 in `SECURITY_HARDENING.md` for the
  full description: `_send_with_retries` validates each cookie
  value before interpolating into the Cookie header; rejects
  via `HttpTransportError` on any CR/LF/NUL; 4 unit tests pin
  the rejection plus the clean-value passthrough.
  (2026-05-17, commit `a7d75d3` — recorded here for tracker-
  coverage parity.)
- **RA-04** — `cause_summary` is now category-only. Reworked
  `_categorise_exc` to return a fixed category string per known
  httpx exception class (`"timeout"`, `"connect-error"`,
  `"network-error"`) and `type(exc).__name__` for the catch-all,
  with NO `str(exc)` interpolation. Same fix on the inner
  `httpx.HTTPError` branch in `_send_with_retries`. The original
  httpx exception still rides through as `__cause__` on the
  wrapping `HttpTransportError`, so a debugger / crash-reporter
  pipeline (which has its own Redactor pass via mgf-common)
  sees the full chain — but the user-facing message that lands
  in consumer logs / CLI output / response bodies no longer
  carries the request URL (which can carry a query-string-
  embedded credential like `?api_key=secret`). 4 new tests
  exercise each branch with a leaky string in the httpx
  exception and assert it does NOT appear in
  `HttpTransportError.cause_summary` or in `str(exc_info.value)`.
  Twin of SH-04 — same fix closes both. (2026-05-17, commit
  pending.)
- **RA-05** — `follow_redirects=` kwarg added to
  `AsyncHttpClient.__init__`. Defaults to `True` (preserves the
  outbound-API-to-provider use case). Consumers handling user-
  supplied URLs (open-redirect class) or high-stakes endpoints
  behind custom auth headers (where httpx does NOT strip
  `X-Api-Key`-shape headers on cross-origin redirects — SH-02)
  can pass `follow_redirects=False` and surface 3xx responses
  themselves. Expanded the constructor docstring with an Args
  block calling out the cross-reference. 3 new tests pin the
  default, the False case threading through to the underlying
  httpx client, and the end-to-end behaviour (302 surfaces as
  the response when redirects are disabled). (2026-05-17,
  commit pending.)
- **RA-06** — `aclose()` idempotency pinned. Added a new
  `TestAcloseIdempotency` test class with four cases: explicit
  double close, explicit triple close, context-manager exit
  followed by an explicit close, and (most realistic) a request
  cycle followed by close + reclose. The pre-existing single-
  line `test_aclose_idempotent` is preserved for backwards
  compatibility but the new class makes the contract explicit
  enough that an httpx version bump that breaks idempotency OR
  a wrapper refactor that adds wrapper-side state would surface
  the regression unambiguously. (2026-05-17, commit pending.)

---

## §6 Notes on methodology

This first pass walked the three source files (~430 LOC
total) end-to-end against:

- **RA** rule patterns from mgf-common's
  `inprogress/RELIABILITY_AND_ACCURACY.md`.
- **EH** rules from `mgf-standard/docs/standards/ERROR_HANDLING.md`
  — especially **EH-11** retry-with-jitter, **EH-12** idempotency
  boundary, **EH-13** circuit-breaker (not yet in scope but
  flagged for follow-up).
- **SC** rules from `mgf-standard/docs/standards/SECURITY_STANDARD.md`
  — secret-shaped header redaction, traceback scrubbing.
- **LG** rules from `mgf-standard/docs/standards/LOGGING.md` —
  redaction in log records, structured fields.

The audit method:

1. Read each source file end-to-end with the rule set in mind.
2. For every concern surfaced, verify `file:line` against the
   live source.
3. Cross-reference against mgf-common's RA tracker AND the
   sister-sibling trackers (mgf-django RA-01 has the same
   "client-supplied header value, defensive validation"
   pattern as the cookie-injection concern here).
4. Triage: items that don't tie to a rule + concrete evidence
   are observations, not tracker items.

Six items + eight invariants is right-sized for a sibling
this focused. Future passes should look for:

- **Provider-specific quirks.** Stripe / GitHub / Cloudflare /
  Clerk each have rate-limit + retry header conventions that
  RA-02 only partially addresses. As consumers integrate
  more providers, the test corpus should grow.
- **httpx version bumps.** Several invariants here delegate
  to httpx's documented behaviour (aclose idempotency, TLS
  verification semantics, header validation). httpx 1.0
  ships with stricter semantics; bump cautiously and re-run
  this audit.
- **Circuit-breaker integration.** **EH-13** mandates a
  circuit breaker for any external call that, when failing,
  can amplify load on the upstream. The library doesn't ship
  one today; depends on whether a consumer surfaces real
  evidence of needing it (memory says it's parked).

When an item closes, move it to §5 with the closing-commit
SHA. When the next pass sharpens an item, rewrite it in place
and note the change in §1.
