Conversation
rbotte
force-pushed
the
feat/gh-json-lossless-pack
branch
from
August 16, 2026 12:20
cbfe933 to
2fdbb12
Compare
gh --json and gh api were passthrough-only (rtk-ai#196): converting JSON to a bare schema would destroy values, so rtk never touched them and tracked 0% savings on exactly the outputs where field names repeat per row. This adds core/jsonpack: a lossless re-encoding ported from the compaction stage of headroom-core (headroomlabs-ai/headroom, Apache-2.0) with every lossy path removed — no retrieval pointers, no row dropping, no stringified-JSON rewriting. - top-level arrays of objects render as a [N]{col:type} declaration plus CSV rows; empty cell = absent key, bare null = JSON null, lookalike strings stay quoted, mixed columns carry JSON literals - envelopes (gh api) stay valid JSON: dense inner arrays become {"_cols":…,"_rows":…} tables and the body is minified - pack() decodes its own output and requires value equality with the input before emitting; any mismatch (including data that collides with the notation) falls back to the raw bytes — Never Block - --jq/--template (long, short, = and attached forms) keep full passthrough; the registry rewrites bare --json to rtk gh again Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Rui Botte <nfsbotte@gmail.com>
Live measurement against real gh outputs showed the level-1 flatten with
headroom's width cap of 6 leaving the heavy payloads untouched: a commit
object has 7 keys, a user ~18, a repository ~45 — so gh api commits/runs
compressed 2-4% while paying CSV quote-doubling on giant json cells.
- flatten recurses (commit.author.name), cap raised 6 → 64; the cap only
guards adversarial blowup, models read wide tables fine
- envelope tables flatten uniform nested objects into dotted _cols,
marked "_flat":1 so the decoder knows dots mean nesting there (plain
tables keep dots literal); our own {_cols,_rows} cells are never
flattened
- CSV stays preferred; the JSON notation is the fallback when CSV cannot
shrink (quote-doubling tax on wide json cells)
- two real-capture fixtures (gh pr list, gh api commits; emails+names
sanitized) pin round-trip and ≥10% savings on bytes gh actually
produced — synthetic fixtures overstate savings
Measured on rtk-ai/rtk: gh api commits 2% → 18%, gh api actions/runs
4% → 20%, gh pr list --json 17%, vs 0% for all of them before this
feature (passthrough).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Rui Botte <nfsbotte@gmail.com>
An adversarial review with a fake gh on PATH proved three HIGH bugs sharing one root cause: switching gh api from passthrough to the shared line-oriented capture made 'fall back to raw' untrue — stdin was cut (--input - sent empty bodies), single lines beyond the 10MiB cap were dropped entirely (empty stdout, exit 0), and binary bodies (tarball) were decoded lossily into mojibake. - run_gh_packed: byte-exact capture used by both packed paths. stdin and stderr stay inherited; strict (non-lossy) UTF-8 or the bytes are emitted verbatim; outputs beyond 32MiB flush what is buffered and stream the rest through untouched — bounded memory, zero truncation - registry: a file redirect on a packed path (--json / gh api) skips the rewrite — a program will parse that file and expects real JSON - wants_jq_or_template: detect pflag fused boolean shorts (-dq expr), where the projection flag hides inside a cluster - flatten: hard 512-column ceiling — the collision scan and per-row splicing are quadratic in width, and 32k-column adversarial input could pin a core for seconds - serde_json gains float_roundtrip: 124342.85041994731 no longer re-emits as …32, so cited digits are the digits the API sent Also survived review: 800k randomized round-trip fuzz iterations with zero verify mismatches and zero panics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Rui Botte <nfsbotte@gmail.com>
The three HIGH bugs the review found were invisible to every unit test: they lived in the plumbing (stdin, buffering, decoding), not in the encoding. Nothing stopped them from coming back. Six integration tests drive the real binary against a fake gh on PATH: stdin reaches gh, a >10MiB single-line body survives whole, a body past the pack cap streams verbatim, a gzip body stays byte-exact, tabular output still packs, and an error body passes through with gh's exit code. Tracking is redirected to a temp DB so runs never touch the developer's analytics. Verified by mutation, not by counting greens: restoring the previous line-oriented capture makes exactly the four fidelity tests fail with the reviewer's own symptoms (stdin_bytes 0, 0x8b decoded to U+FFFD, 0 bytes emitted for 34MB) while both controls keep passing. That run also exposed a weak assertion of my own — a 2.7MB payload passed even against the broken capture, so the fixture now exceeds the 10MiB cap it is meant to probe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Rui Botte <nfsbotte@gmail.com>
rbotte
force-pushed
the
feat/gh-json-lossless-pack
branch
from
August 16, 2026 13:07
2fdbb12 to
54cc0bf
Compare
Author
|
Ready for review whenever it fits your queue — posting status so triage is cheap:
The only thing outstanding is the first-time-contributor workflow approval, which needs a maintainer click. Happy to rebase or split the PR if that helps. |
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.
What
gh <cmd> --json …andgh api …are currently full passthrough (#196), tracked at 0% savings — exactly the outputs where most bytes are field names repeated on every row. The oldrun_apicomment states why: "Converting JSON to a schema destroys all values and forces Claude to re-fetch." That objection is to lossy conversion.This PR adds
core/jsonpack: a lossless re-encoding that keeps every value and removes only the repetition:Top-level arrays of objects → CSV+schema:
[N]declares the row count (truncation is detectable),?marks nullable columns, an empty cell means the key was absent, barenullmeans JSON null, lookalike strings ("null","42"in string columns) stay quoted, mixed-type columns carry JSON literals so42and"42"can never blur. Uniform nested objects flatten recursively into dotted columns (commit.author.name).Envelopes (
gh apiobjects) stay valid JSON: dense inner arrays of objects become{"_cols":[…],"_rows":[[…],…]}tables (uniform nested objects flatten into dotted_cols, marked"_flat":1), and the body is minified.--jq/--template(long, short-q/-t,=and attached forms) keep full passthrough — the caller asked for a projection. The registry rewrites bare--jsontortk ghagain.Losslessness is verified at runtime, not assumed
pack()decodes its own output and requires exact value equality with the parsed input before emitting a single byte. Any mismatch — encoder bug, or adversarial data that collides with the notation itself (a real{"_cols":…,"_rows":…}object in the payload) — returns the raw bytes unchanged. Tests include those adversarial cases. This honors Never Block, composes with the existingnever_worsetoken guard, and means the worst case of this feature is the status quo (raw passthrough).The only normalization is serde_json number parsing (
1.10→1.1), shared by every JSON path in rtk today.Measured (real data, rtk-ai/rtk)
gh api repos/rtk-ai/rtk/actions/runs?per_page=12gh api repos/rtk-ai/rtk/commits?per_page=25gh pr list --limit 50 --json number,title,state,author,updatedAt,urlgh issue list --limit 30 --json …gh api repos/rtk-ai/rtk(single object)Real payloads are value-dominated (URLs, ids, timestamps), so the honest lossless ceiling here is well below the synthetic-fixture numbers — the two committed real-capture fixtures (produced by gh, emails sanitized) pin round-trip + ≥10% savings on actual bytes, precisely so synthetic fixtures can't overstate.
Design notes
--jsonpassthrough and only packinggh api, the gate is one line ingh_cmd::run.labels) still render as JSON cells and pay CSV quote-doubling — nested sub-tables would liftgh issue listbeyond 4%;glab apicould reuse the packer as-is.Capture is byte-exact on the packed paths — "raw fallback" means raw bytes
An adversarial pre-submission review (fake
ghon PATH, end-to-end through the hook) drove the third commit. The shared line-oriented capture would have made "fall back to raw" untrue forgh api: stdin cut (--input -sending empty bodies), single lines beyond the 10MiB cap dropped entirely (empty stdout, exit 0), binary bodies decoded lossily. The packed paths therefore use their own byte-exact runner: stdin/stderr inherited, strict UTF-8 or verbatim bytes, and outputs beyond 32MiB stream through untouched with bounded memory. Additionally: a file redirect (--json … > f.json/gh api … > f.json) skips the rewrite at the registry (a program will parse that file); pflag fused shorts (-dq 'expr') are detected as projections; the flatten pass has a hard 512-column ceiling (its width cost is quadratic); and serde_json gainsfloat_roundtripso cited float digits are exactly the digits the API sent.Tests
jsonpack(round-trips for commas/quotes/CRLF/unicode, null vs absent vs empty vs lookalikes, extreme numbers, recursive flatten with missing parents, adversarial notation collisions, strict decoder rejections, column-cap bailout, float digit fidelity) + gh_cmd gate tests + registry tests (incl. redirect skip).tests/gh_pack_process_fidelity_test.rs: 6 integration tests driving the real binary against a fakeghon PATH — stdin reaches gh, a >10MiB single-line body survives whole, a body past the pack cap streams verbatim, a gzip body stays byte-exact, tabular output still packs, error bodies pass through with gh's exit code. These cover the plumbing, where unit tests are blind and where all three HIGH bugs lived.stdin_bytes0,0x8bdecoded to U+FFFD, 0 bytes emitted for a 34MB body) while both controls keep passing.cargo fmt --check,clippy --all-targets(deny warnings),scripts/check-test-presence.sh origin/developall pass.🤖 Generated with Claude Code