Fix all 42 findings from the 2026-06-10 codebase audit#31
Merged
Conversation
The global MaxBytesBody middleware deliberately exempts all /peeringdb.v1.* paths so server-to-client streaming responses are not truncated, but nothing replaced that cap at the connect protocol layer. connect-go's default ReadMaxBytes is zero, which permits messages of any size, and the limit also governs the decompressed size of compressed requests. A single oversized or gzip-bombed unary POST to any of the unary endpoints therefore buffered fully in heap inside the 30s read window — enough to OOM-kill a 256 MB replica with one request. Add connect.WithReadMaxBytes(maxRequestBodySize) to the shared handler options, extracted into connectHandlerOpts so the wiring is testable. Request messages on these services are small filter/pagination structs, so the 1 MB cap shared with the HTTP-level middleware is generous. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
restFieldRedactMiddleware only buffered /rest/v1/ix-lans* responses and only inspected top-level JSON keys. entrest eager-loads the ixlan edge unconditionally for both list and detail operations, so every endpoint that embeds an ixlan — internet-exchanges, ix-prefixes, network-ix-lans — returned the tier-gated ixf_ixp_member_list_url unredacted under edges.ix_lans / edges.ix_lan, including to anonymous callers on the default public deployment. Broaden the middleware to all /rest/v1/ paths and locate gated objects by a recursive walk keyed on the _visible companion's presence rather than by response shape. This also retires the restListWrapperKey constant whose lock-step coupling to entrest's PagedResponse tag was itself a documented leak hazard. New E2E sub-tests cover the embedded-edge shapes: anonymous callers must not see the gated URL under edges.ix_lans on internet-exchange detail and list responses, and users-tier callers must still see it there (guarding the walker against over-redaction). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A bare /api/<type> list contains only status='ok' rows (upstream filters bare lists), and committing a full snapshot advances the derived MAX(updated) cursor past the pre-cycle window. Every full-mode fetch — the daily PDBPLUS_FULL_SYNC_INTERVAL escalation and the per-type incremental-fallback — therefore silently discarded any deletes that landed upstream inside that window. With inference-by-absence deliberately removed, nothing ever healed those rows: they stayed 'ok' across all five API surfaces forever and never appeared in our own ?since= exports. Stage a follow-up ?since=<cursor> fetch on top of the bare snapshot whenever the table has a non-zero cursor. The scratch table's INSERT OR REPLACE is keyed on id, so window rows — including tombstones — win over their bare-list versions. In explicit full mode a window-fetch failure fails the type so the cycle retries instead of advancing the cursor past unseen deletes; on the incremental-fallback path the fetch is best-effort (it is the same request shape that just failed) and a second failure is logged and tolerated so the fallback keeps its purpose. StreamAll's since-pagination now stops on a short page rather than only on an empty one. Upstream applies its status filters before limit/skip, so every non-final page is exactly pageSize rows; this saves the trailing empty-page request on every incremental fetch and keeps naive single-payload test stubs terminating. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gqlgen's default complexity charges 1 per field regardless of list fan-out, so FixedComplexityLimit(500) bounded the number of fields in the query text, not the rows it materializes. Nested unpaginated edge lists multiply: internetExchanges(first:1000) expanding ixLans -> networkIxLans -> ixLan -> networkIxLans costs ~10 units under default costing yet materializes millions of ent rows plus their JSON — an OOM-kill on the 256 MB replicas. Register complexity functions for every Relay connection and offset/limit flat list (multiplying by the requested page size, with the resolvers' own defaults) and for every unpaginated entity edge list (multiplying by an order-of-magnitude weight for the live corpus's average per-parent cardinality, with the heavy-tailed ixlan.networkIxLans member list weighted highest). The limit budget rises from 500 to 1,000,000 because the unit changes from fields mentioned to approximately fields-of-rows fetched: a full first:1000 page of networks costs ~50k and passes, a single exchange's complete member list costs ~30k and passes, and the nested-expansion-of- everything shapes are rejected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The depth>=2 branches of the org, net, ix, carrier, and campus getters
eager-loaded their reverse sets with bare With*() calls, so the DEFAULT
detail shape (depth=2) serialized status='deleted' rows into the _set
arrays as if they were live — re-publishing deleted POC contact data
indefinitely, among others. Upstream prefetches nested sets filtered to
status="ok" (peeringdb_server/serializers.py:928), and our own depth-1
ID-list builders and the ixlan getter already filtered StatusIn("ok",
"pending"), so the unfiltered eager loads were both a parity break and
an internal inconsistency.
Filter every depth>=2 reverse-set eager load with StatusIn("ok",
"pending"), matching the live-validated depth-1 choice. To-one FK
expansions stay unfiltered, as before (an ok row's parent is ok by
sync-order construction).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three divergences on the incremental-update path that peeringdb-py style pollers depend on: The since matrix activated for any parseable value, but upstream gates it with 'if since > 0' (rest.py:696) — ?since=0 there serves the plain status='ok' list, while the mirror returned the entire tombstone corpus. ParseSinceParam now treats since<=0 as absent. The window filter used updated >= since, but django-handleref's since() filters strictly greater. A client polling with the max updated value it had already seen re-received every boundary row on each poll. applySince now uses GT; updated__gt alone subsumes created__gt because created <= updated on every row. Since responses kept the newest-first default ordering, but upstream returns incremental updates ordered by updated ascending so pollers can resume from the last row they processed. A listOrder helper now picks updated-ascending (id tiebreak) whenever ?since= is present, across all 13 list closures. New parity sub-tests lock each behaviour with upstream citations; the existing since=0 test fixtures move to since=1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No web-UI query filtered on entity status, so soft-deleted tombstone
rows — written by the sync worker for upstream deletions and never
garbage-collected — rendered as live data everywhere: search results,
the per-type listing pages, detail pages reached by ASN or ID, all
lazy-loaded detail fragments, and the comparison view. The templates
never render the status column, so a deleted entity was
indistinguishable from a live one, and the UI silently disagreed with
the same deployment's /api/ surface about what exists.
Add a StatusIn("ok", "pending") predicate to every entity query in
internal/web (52 sites), filtering each traversal hop that produces
rendered rows (e.g. ix -> ixlan -> netixlan participants). List/count
pairs keep identical predicates, mirroring the pdbcompat
shared-predicates invariant. The sync_status freshness reads are not
entity tables and stay unfiltered, as do WithX() eager-loads that only
resolve display labels for already-filtered rows.
The regression test seeds ok + deleted siblings and locks: search
excludes the deleted network, the deleted network's ASN 404s while the
live sibling renders, and the IX participants fragment excludes a
deleted netixlan.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ParsePaginationParams silently ignored non-numeric limit/skip values (treating them as absent) and clamped explicit limits above 1000 down to MaxLimit. Upstream 400s non-numeric values (rest.py:490-497, "'limit' needs to be a number") and slices qset[skip:skip+limit] with no upper cap (rest.py:734-735). The silent-ignore turned a typo'd limit into a full-table dump; the clamp silently truncated every page for clients paginating with windows larger than 1000 — rows past the clamp were permanently skipped with nothing in the envelope to signal it. Return 400 application/problem+json naming the offending parameter for non-numeric (and negative) limit/skip, and honour large explicit limits unmodified — the response-memory budget remains the actual cost bound, exactly the rationale that removed the old 250-row default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream special-cases bare location filters before generic handling (rest.py:562-574): address1, city, and state become <field>__icontains substring matches, and country becomes __iexact for 2-char values or __icontains for longer ones. The mirror routed all bare string filters through exact matching, so GET /api/fac?city=Frankfurt returned only rows whose city is exactly "Frankfurt" while upstream also returns "Frankfurt am Main" — geographic tooling ported from upstream silently got fewer rows with no error. Coerce the operator in buildLocalPredicate, after the field-existence check. Only bare local filters coerce: upstream's special-case list matches the raw param name, so explicit operator suffixes and relation prefixes take the generic path there too. The 2-char country case needs no rewrite because the exact builder's string branch is already case-insensitive. The folded city columns keep their _fold routing via the existing buildContains path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Time-typed filters (created/updated/rir_status_updated/...) parsed values with an epoch-seconds-only parser, so every upstream-style query like ?updated__gt=2024-01-01 returned 400 — no time-filter syntax worked against both systems, and bare date equality (a natural query) was unexpressible. parseTimeValue now accepts the ISO 8601 layouts DRF's DateTimeField accepts upstream (date-only, datetime with T or space separator, RFC 3339 with offset) alongside epoch seconds, and carries upstream's two semantic layers (rest.py:619-658): a bare 10-char date in gt/lte gets its time forced to end-of-day (updated__gt=2024-01-01 means "after that whole day"), and bare date equality becomes a [00:00, 24:00) day-window range, mirroring the __startswith rewrite. ?since= deliberately keeps the strict epoch parser (parseEpoch): upstream coerces since with int(), so an ISO value 400s there and must keep 400ing here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildIn's string branch compared raw CSV values against the column under SQLite's BINARY collation, so __in was case- and diacritic-sensitive while every sibling operator on the same field (exact, contains, startswith) folds case and routes folded fields through the <field>_fold shadow column. ?name=decix matched "DECIX" but ?name__in=decix missed it. Upstream applies unidecode to ALL filter values including __in (rest.py:576) and matches under MySQL's case-insensitive collation. Thread the folded flag into buildIn; string lists lower-case each element (after unifold.Fold when the field is folded) and compare LOWER(col) — or LOWER(<field>_fold) — against the json_each set. Int lists keep the bare numeric comparison. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three related caching-header defects from the 2026-06-10 audit:
The caching middleware stamped public Cache-Control (sync interval +
120s) and the sync-keyed ETag unconditionally before dispatch, so
every 400/404/413/500 was publicly cacheable for over an hour, and
/healthz and /readyz responses — including 503s — could be pinned by
shared caches. Worse, a conditional GET with a matching If-None-Match
returned 304 from the middleware without ever running the readiness
probes, so revalidating monitors were told their cached 200 was still
valid on an unhealthy node. Error responses now pass through a
cacheStripWriter that downgrades to no-store at WriteHeader time for
any status >= 400, and /healthz + /readyz join the no-store skip list
(which also bypasses the 304 short-circuit).
Web render paths and the root discovery handler called
Header().Set("Vary", ...), clobbering the Vary: Accept-Encoding that
gzhttp adds before dispatch — with public Cache-Control on the same
responses, a shared cache could replay a gzipped variant to clients
that never sent Accept-Encoding. All sites now Add instead of Set,
and the root handler's default branch (which negotiates on both
Accept and User-Agent) gains the Vary it was missing entirely.
Web 404/500 pages called WriteHeader before renderPage set
Vary/Content-Type; net/http drops header mutations after WriteHeader,
so non-gzip clients got no Vary and a body-sniffed text/plain
Content-Type on JSON problem bodies. The status now travels in
PageContent.Status and renderPage commits it after the headers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generated-code drift check scoped its diff to ent/ gen/ graph/ internal/web/templates/, but go generate also rewrites internal/pdbcompat/allowlist_gen.go — the security-load-bearing /api traversal allowlist that CLAUDE.md and DEVELOPMENT.md both claim the gate protects. Editing ent/schema/pdb_allowlists.go without regenerating shipped a stale allowlist through CI silently. Add the file to the diff pathspec and stage intent for untracked files first so newly created generated files fail the gate instead of being invisible to git diff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 13 hand-written *List resolvers and NetworkByAsn built bare ent
queries and returned query.All(ctx) without CollectFields, so every
nested edge selection fell into ent's generated per-row lazy-load
path: networksList(limit:1000){organization{name}} issued 1 + 1000
sequential SQLite queries, each through database/sql + otelsql — and
when sampled, 1001 spans toward Tempo's per-trace cap. The relay
connection resolvers were already safe because entgql's Paginate calls
CollectFields internally.
Chain CollectFields onto each query so requested edges eager-load in
O(edges) batched queries. NetworkByAsn additionally gains the
StatusIn("ok", "pending") filter the /api/ and /ui/ surfaces
already apply — the curated by-ASN lookup no longer resolves
soft-deleted tombstones.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Worker.synced gates every data route via readinessMiddleware and the gRPC health poller, but it was evaluated from sync_status exactly once at StartScheduler entry. A replica that booted before the primary's first successful sync — fresh-fleet bootstrap, wiped-primary recovery, or a transient read failure at boot — latched unready permanently: 503 on all data routes and NOT_SERVING health for the life of the process, even after LiteFS replication delivered a fully-synced database. While the latch is unset, the replica heartbeat now re-reads sync_status each interval and flips to ready when a successful sync appears. The read is a single local-SQLite row, paid only while unsynced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cache_size is per connection, so the 32 MB setting multiplied by the pool: MaxOpenConns=10 capped page cache alone at 320 MB — more than an entire 256 MB replica VM — and the 5 idle connections retained up to 160 MB for ConnMaxLifetime after a read burst. The sizing comment reasoned only about the primary's single-connection bulk-upsert burst under the 512 MB cap; replicas run the same Open() and serve the read-heavy full-dump traffic that actually warms ten separate caches. 8 MB per connection bounds the pooled worst case at 80 MB while still quadrupling SQLite's ~2 MB default for the upsert transaction's page reuse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
skipUnchangedPredicate's godoc claimed full-mode runs ignore the updated-timestamp gate and reconcile completely, but the predicate was attached unconditionally in all 13 upsert closures — no mode ever reached upsert.go. Rows the sync mutated locally without bumping updated (facility campus_id or netixlan side FKs nulled by the orphan filter) therefore never re-converged with upstream: excluded.updated equalled the stored value, so the DO UPDATE was skipped on every subsequent cycle including the daily forced full whose documented purpose is exactly this reconciliation. Newly added _fold columns would likewise never backfill on existing rows. Carry a reconcile-all marker on the cycle context (cycle-scoped data, set once in syncCycle for full-mode runs) and have skipUnchangedPredicate emit an always-true UpdateWhere when present, keeping the 13 OnConflict call sites uniform. Incremental cycles keep the optimization and its documented bounded same-second-drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CheckBudget gated each list request in isolation against the full PDBPLUS_RESPONSE_MEMORY_LIMIT, with no process-wide accounting of in-flight responses. serveList fully materializes results before streaming, and fly.toml's Fly-Proxy defaults allow 200 concurrent requests per replica — so two concurrent near-budget dumps, each individually under the 128 MiB budget, could jointly materialize more than an entire 256 MB replica and OOM it. Admission now also charges the request's estimated bytes against a shared atomic in-flight pool: when the pool would exceed the budget the request is rejected with 503 + Retry-After: 1 instead of stacking, and the charge is released when serveList returns. The architecture's 413 semantics for single oversized requests are unchanged — the 503 is specifically "try again shortly, the server is busy serving other large responses". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The short-page break added to StreamAll's since-pagination left TestFetchAllRateLimiter and TestFetchAllPagination expecting the old trailing empty-page request: their stubs returned fewer than pageSize rows on every page, so the request counts and rate-limit timing they asserted no longer matched. Serve full pages with a short final page and update the expectations to the new (one fewer request) contract. Also drop the three FetchType tests ahead of the dead FetchType removal in a following commit — they were the function's only callers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four operability gaps from the 2026-06-10 audit's low-severity batch: A sync trigger arriving while a cycle was already running was dropped by the running-CAS guard with a silent nil return — the operator's ?mode=full escape hatch could no-op invisibly. The guard now logs the drop with the requested mode and returns ErrSyncAlreadyRunning; SyncWithRetry propagates it without entering the retry ladder, and the new Worker.Running() probe lets the HTTP handler answer 409. SyncWithRetry retried into upstream WAF blocks; IsWAFBlocked existed with a godoc claiming the worker used it, but had zero callers. The ladder now short-circuits on WAF detection — retrying into a WAF only digs the hole deeper. The per-chunk backfill pre-pass ran even with the backfill cap set to 0 (backfill disabled), emitting misleading 'fk backfill cap reached' warnings and rate-limited metrics every cycle. It now returns early. Terminal sync_status writes swallowed errors entirely, leaving rows stuck 'running' with no signal; failures now log at ERROR with the status id and outcome. Comments claiming dropped orphans are 'retried next cycle' were corrected: recovery comes from the next full-mode cycle, which since this branch re-fetches every row, stages the tombstone window, and bypasses the upsert skip gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five fixes from the 2026-06-10 audit's low batch, all in the main package: On-demand syncs (POST /sync) ran bare SyncWithRetry with none of the demotion monitoring the scheduler's cycles get — a node demoted mid-cycle kept burning upstream PeeringDB quota through the rest of the cycle and retry ladder. They now run under the same 1s-poll demotion monitor pattern. POST /sync answered 202 Accepted even when the worker would drop the trigger because a cycle was already in flight; the handler now probes Worker.Running() and answers 409 with a JSON conflict body. A ListenAndServe failure called os.Exit(1) inside the serve goroutine, skipping every deferred cleanup — including the OTel flush carrying the log records that explain the failure. Serve errors now propagate through a channel to main's normal return path so the defers run. restErrorMiddleware rewrote every entrest error to a generic problem+json body, discarding the validation detail clients need to fix their request. 4xx responses now carry entrest's error detail; 5xx detail stays generic (no SQL/driver internals on the wire). The served OpenAPI spec documented entrest's error schema, which the middleware guarantees is never emitted. The spec is patched at startup to declare application/problem+json error responses instead. The startup warning (and CONFIGURATION.md) describing an empty PDBPLUS_SYNC_TOKEN as leaving /sync open now states the real fail-closed behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fly.toml's HTTP health check hit /healthz, which always returns 200 —
so the documented Fly Proxy exclusion of cold-syncing replicas never
happened; hydrating machines took traffic and served 503s. The check
now hits /readyz, which fail-closes during hydration and on stale
data. DEPLOYMENT.md updated to match.
Dockerfile.prod built without CGO_ENABLED=0, contradicting the
documented pure-Go build and diverging from the dev image.
DEVELOPMENT.md's 'Adding a new ent field' procedure told contributors
to hand-edit ent/schema/{type}.go — a file cmd/pdb-schema-generate
rewrites on every go generate, silently stripping the edit. Rewritten
around the real paths: schema/peeringdb.json for upstream-derived
fields, sibling-file mixins for local ones.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream's unidecode folds these code points but NFKD leaves them intact, so ASCII queries failed to match affected names through the _fold shadow columns: a search for 'oeuvre' missed 'Œuvre', 'd' forms missed Icelandic eth names, and Turkish dotless-i names were unreachable. Add the three pairs to the hand map with table-driven coverage plus an end-to-end fold-filter round trip. Existing _fold column values containing these runes stay stale until their rows re-upsert; the next full-mode cycle (which now bypasses the upsert skip gate) converges them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
toMap serialized every nested-set element through a JSON
marshal+unmarshal round trip on the default depth-2 detail path. It
now reuses the cached reflect field map from the search path, with an
encoding/json-mirroring emptiness check so omitempty behaviour — load-
bearing for the redacted ixf_ixp_member_list_url key — is preserved
byte-for-byte (locked by a wire-equivalence test).
Also close three gaps in the divergence registry contract: the /api/
error envelope (RFC 9457 problem+json instead of upstream's
{"meta":{"error":...}}) was an intentional divergence registered
nowhere; it now has a DIVERGENCE_ parity test and a Known Divergences
row. The poc_set-privacy and fold-window rows claimed guarding tests
that did not exist; both are now test-locked, so docs/API.md's 'every
row has a guarding test' claim holds again. The ligature list in the
Validation Notes gains the new unifold pairs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The grpcserver stream-cursor wire codec (encodeStreamCursor, decodeStreamCursor, parseCursorTime) had no production callers and its doc comments described a page_token wire contract that does not exist — the cursor is in-memory state inside StreamEntities only. The type doc now says so. peeringdb.FetchType had zero callers outside its own tests; one doc comment claimed a /readyz role it never had. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pdb() wrapper in both the bash and zsh scripts expanded arguments with "$@", so 'pdb asn 13335' ran curl with two separate URLs — the bare ID fetched as a second request against the current directory host. Join arguments with IFS=/ instead. The zsh _arguments spec was single-quoted, so $subcmds never expanded and completion offered the literal string. Tests assert the script content and (hermetically, with a stubbed curl) the URL a real bash/zsh constructs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The upstream-host refusal (never point the generator at www.peeringdb.com, whose 1 req/hour/IP cap would block the operator) only ran in ramp mode; endpoints, sync, and soak could still target upstream. The guard now runs right after flag parsing for all modes. Ramp's no-inflection path discarded every measured step except the baseline, so a probe that never found its inflection point reported almost nothing. All measured steps now appear in the markdown table, with the final row labelled max-concurrency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gqlgen's config loader takes the target package name from the alphabetically-first .go file in the exec/model directory. collectfields_test.go (package graph_test) sorted before custom.resolvers.go, so 'go generate ./graph' — and therefore CI's drift gate — failed with 'exec and model define the same import path (graph vs graph_test)'. Rename the file to resolver_collectfields_test so the first file stays in package graph, and record the sharp edge in CLAUDE.md. Full 'go generate ./...' now converges with zero drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A full-codebase audit (multi-agent: 10 dimension finders, dedup, adversarial verification) confirmed 42 findings — 4 high, 16 medium, 22 low. This branch fixes all of them across 28 commits, one logical change per patch.
Highs
ixf_ixp_member_list_urlinside eager-loaded edges — it leaked to anonymous callers via internet-exchanges / ix-prefixes / network-ix-lans (privacy)WithReadMaxBytes) — unary bodies were unbounded incl. decompression (gzip-bomb OOM)?since=<cursor>tombstone window during full-mode staging — daily forced-full cycles permanently discarded the window's deletesMediums (16)
pdbcompat parity (
sincegate/boundary/ordering, limit/skip validation + clamp removal, location-filter icontains coercion, ISO 8601 time filters with day-window semantics, case-insensitive fold-routed__in), tombstone visibility (depth≥2 sets, all web-UI queries,networkByAsn), caching/Vary/WriteHeader-order fixes, replica readiness-latch recovery, full-mode reconcile-all (upsert skip-gate bypass), pooled SQLite page-cache sizing, global in-flight response-budget admission, GraphQLCollectFieldsbatching, CI drift gate coveringallowlist_gen.go.Lows (22)
/sync409-on-busy + WAF retry short-circuit + status-write error logging, serve-failure OTel flush, REST error detail passthrough, OpenAPI error-schema accuracy,fly.toml/readyzchecks,Dockerfile.prodCGO_ENABLED=0, DEVELOPMENT.md procedure rewrite, unifold œ/ð/ı, divergence-registry completeness, dead-code removal, shell-completion fixes, loadtest upstream guard + ramp reporting.Verification
go build,go vet,golangci-lint(0 issues), andgo test -race ./...all green on the full tree; every behavioral fix carries a regression test (privacy e2e, parity tests with upstream citations, tombstone-window worker tests, complexity limit tests, etc.).Post-deploy
Trigger one
?mode=fullsync to converge stale_foldvalues and heal orphan-nulled FKs (full mode now genuinely reconciles). Full cycles issue 13 extra?since=requests (~7–13 s of rate-limit budget).🤖 Generated with Claude Code