Skip to content

feat: send the flow bundle as one xz document per side - #171

Merged
Svilen-Stefanov merged 9 commits into
mainfrom
feat/flow-side-document
Nov 12, 2026
Merged

Svilen-Stefanov merged 9 commits into
mainfrom
feat/flow-side-document

Conversation

@Svilen-Stefanov

@Svilen-Stefanov Svilen-Stefanov commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Large repos get no flows or merge risk. On CodeBoarding/crate#1 (run 42718797329) they were skipped because "its bundle is 24.6 MB, over the service's limit" of 4.3 MB. The bundle sent every fact about four times:

  • 149,315 of crate's 149,316 analysis edges are also call-graph edges;
  • nested relations repeat the top-level edges;
  • the methods index and the graph's nodes are one table.

Stacked on #174 (feat/analysis-json-v1-reader): it reads analyses through #174's analysis_reader. Retarget to main once #174 merges.

Needs CodeBoarding/licensing-aws#56 deployed first: only that service reads xz and side documents. App link: https://app.codeboarding.org/CodeBoarding/licensing-aws/pull/56

What changes (scripts/remote_features.py, formerly scripts/closed_features.py)

  • Renamed to remote_features.py, and rewritten to be readable. The module docstring now says what a request sends, why it is packed this way, and its size limits. The step is "Run remote features". The closed_features_source input and CLOSED_FEATURES_SOURCE stay as they are, since workflows already set them.

  • call_graph() returns only what the service reads: each node's file, span and callback flag, and each call's lines (no callable, no site file or column).

  • side_document() writes each commit as one document, head.json and base.json. These replace analysis.json, base/analysis.json, head-cg.json and base-cg.json. Each holds:

    • a row per method or graph node;
    • calls by caller, with their lines;
    • the component tree, with members as row numbers;
    • only the analysis edges that are no call (extra_edges): 1 of 149,316 on crate's base. The service reads the rest as calls.
  • Analyses are read through analysis_reader (fix: build the review diff from Core's parsed analysis, not the document #174), so a side document is the same whether Core wrote the legacy or the "v1" analysis.json.

  • xz -9e instead of gzip. xz's window spans the bundle, so the base side costs little next to a head that repeats most of it. problem_history.py's briefing request uses it too.

  • Checked against both of the service's size limits before sending. _encode measures the JSON first, which is cheap, then compresses once:

    • a feature request: 256 MiB of JSON, 4.3 MB compressed;
    • a briefing: 32 MiB of JSON, 4.3 MB compressed.

    The constants name their source, MAX_BUNDLE_BYTES and MAX_BRIEFING_BYTES in licensing-aws closed_functions/handler.py. Before this, only the compressed size was checked. A briefing of 32–43 MiB of JSON, which xz takes under 4.3 MB, was sent whole, refused, and sent again on every sync.

  • Each feature request is compressed once, one at a time. Both features can send the body without source, so it is compressed once. It says why source is missing, which only architecture reads.

    • Architecture's body with source is compressed only when there is source. When that doesn't fit, it sends the shared bytes.
    • That is 1 compression without source and at most 2 with it, never two at once. Before, there were 2 or 3, two of them in parallel at about 700 MB each.
    • The 256 MiB JSON limit is 62 times the compressed one, so the cheap JSON check already turns away anything that could only fit at better than 62:1. No tighter cut-off holds for every request.
  • The briefing leaves findings out against the JSON limit first, largest envelopes first, counted per entry without serialising again. Then it compresses once. Only when the compressed size is still over 4.3 MB does it compress again, down to 90% of what the measured ratio says fits, at most 3 compressions in all. It used to recompress once per dropped finding.

    • Measured: 35.5 MiB of findings JSON went out as 31.9 MiB (2.99 MB xz) after 1 compression, in 19 s. 4 of 60 findings were left out.
  • MAX_FILE_BYTES (300 KB, the limit source already had) now also applies to changed files. A changed file over it is left out at the commit where it is that large; the other commit still sends it. Until now files.json capped the file count, not bytes.

Behaviour change: a call to a class is listed once, at its constructor

In the review guide, a call to a Python or TS class is now listed only as Class.__init__ (or constructor). Before, it was listed twice, as Class.__init__ and as Class.

  • Where the two came from: the guide's cross-component calls join two sources.
    • The call graph: the guide's callee_key sends a call into a class to its constructor, so it gives Class.__init__.
    • The analysis's relation edges: they are not redirected, so they gave Class.
  • Why one is gone: this PR sends only the analysis edges that are no call. caller → Class is a call, so it no longer arrives as an analysis edge. Only the call graph's Class.__init__ remains.
  • Why this is right: both entries described the same call. The guide's own rule is that calling a class lands at its constructor.
  • Nothing else changes:
    • a class without a constructor is still listed as Class;
    • an analysis edge into a class that is no call is still sent, and listed as before.
  • Measured on CodeBoarding's own analysis: 91 analysis edges go into a class that has a constructor. All 91 are calls, so each is now listed once instead of twice.
  • Why crate didn't show it: Java constructors are not among the guide's constructor names.

Measured on crate

Today This PR
Bundle 24.6 MB gzip 1.41 MB xz (7 s to compress)
Decompressed 717 MB 14.6 MB
Action peak memory, both sides held at once 2.0 GB
  • Same guide: read by the service PR's own read_bundle, the guide holds the same content as from today's bundle. The order of some lists follows the call order and differs. Architecture's file graphs are identical.
  • Lossless writer and reader: side_document read back by the service's expand_side reproduces every field a feature reads on 40 cases; of the analysis's edges, those that are no call. That covers crate's two sides with the real graph, and 18 local analyses with no graph and with a synthetic one, including methods sitting in two leaves.
  • Legacy and v1 agree: the test fixture is one analysis written by Core's two writers: legacy at main 53f0bb8e, "v1" at 3adb6969, both after align_relation_edges. Both give the same side document. The rename and rewrite gave the same bytes as before them.
  • Not verified:

Test plan

  • python -m unittest discover -s tests: 355 OK, 7 skipped
  • black 25.9.0
  • New tests:
    • the exact side document for a small analysis and graph (the service's tests read the same literal back);
    • a v1 analysis gives the same side document as its legacy one, both generated by Core's writers;
    • a parent's members are left out only when its children's own members rebuild them;
    • a changed file over the size limit is left out only at that commit;
    • findings over the briefing's JSON limit are left out before compressing, with one compression;
    • findings over the compressed limit are left out, largest first, within 3 compressions;
    • without source both features send one body, compressed once; with source architecture's is compressed once more;
    • a feature request over either limit drops source.json first and sends the guide's bytes, then is skipped;
    • bodies decode as xz.
  • After the service deploys: one review run on Fix approximation of number of distinct values (replay of crate/crate#20565) crate#1 shows flows

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T21:21:32.359669Z 23386ca New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codeboarding-review

codeboarding-review Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

CodeBoarding review

Status: 2 changed components

See the full change in CodeBoarding.

Compared against the merge base: this branch is 1 commit behind feat/analysis-json-v1-reader.

graph LR
    n_Action_Analysis_and_Authentication_Tools["Action Analysis and Authentication Tools"]
    n_Remote_Review_and_Briefing_Sync["Remote Review and Briefing Sync"]
    n_Remote_Review_and_Briefing_Sync -- "invokes GitHub client utilities and payload han…" --> n_Action_Analysis_and_Authentication_Tools
    classDef added fill:#1f883d,stroke:#0b5d23,color:#ffffff;
    classDef modified fill:#bf8700,stroke:#7d4e00,color:#ffffff;
    classDef deleted fill:#cf222e,stroke:#82071e,color:#ffffff,stroke-dasharray:5 3;
    class n_Action_Analysis_and_Authentication_Tools,n_Remote_Review_and_Briefing_Sync modified;
    linkStyle 0 stroke:#7d4e00,stroke-width:2px;
Loading

download artifacts · run 43300709025

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83e64a45e6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread scripts/problem_history.py Outdated
findings, dropped = dict(body.get("findings") or {}), []
while True:
data = _gzip({**body, "findings": findings})
data = _compress({**body, "findings": findings})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid recompressing once per dropped finding

When a briefing request exceeds 4.3 MB by many similarly sized findings, this loop now runs the expensive xz preset=9 | PRESET_EXTREME compression once for every PR it removes. Since a request can contain up to 200 findings and the script has a 600-second hard watchdog, a sufficiently oversized request can time out before it ever posts or commits the ledger, causing the same findings to be retried on every sync. Remove findings in batches or use a bounded/binary search so fitting the request requires only a few compression passes.

Useful? React with 👍 / 👎.

@ivanmilevtues ivanmilevtues left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is really difficult to udnerstand what is happening from the code itself, so that is why i wrote the comments. Otherwise from what i got it is mainly changing the compression algo + being slightly smarter about how we go about sending things.

#!/usr/bin/env python3
"""Call the review-guide and architecture endpoints independently after the review analysis.

Both requests carry the same analyses, call graphs, PR metadata and changed files. Only architecture

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hate the naming of this file. I would call it remote_faetures or anything better, they are not closed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, renamed to remote_features.py in 005b3a4. The step is now "Run remote features" (id: remote_features), and problem_history.py and the tests import it under the new name. I kept the closed_features_source input and CLOSED_FEATURES_SOURCE env as they are, since workflows already set them. The thread is on the old path, so the file now lives at scripts/remote_features.py.

"file": n.file_path,
"start": n.line_start,
"end": n.line_end,
"callable": bool(n.is_callable()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file in general looks like it is minified. Maybe we should reprompt to not use a single letter paratemeters and so on.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, it was hard to follow. 005b3a4 rewrites it rather than patching it:

  • descriptive names, no single-letter parameters;
  • short functions: _Table/_Row for the file table, _calls, _components, _flatten;
  • a module docstring that says what a request sends, why it's packed this way, and the size limits.

Behaviour is unchanged: the bundle was byte-identical before and after on 42 cases, crate included.

Since your approval:

@Svilen-Stefanov Svilen-Stefanov left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self before merging:

  • Retarget to main once #174 is in (it's still stacked on feat/analysis-json-v1-reader).
  • The size check inline, that one is the actual fix.
  • PR body is a bit off: an oversized file is dropped per side, not "at neither commit", and the "refused key" test it lists doesn't exist anymore.
  • Reply to Ivan on his two threads, his approval is from before the rename.
  • Mention the behaviour change: a call to a Python/TS class now lists only Class.__init__, not both Class.__init__ and Class.

Comment thread scripts/problem_history.py Outdated
findings, dropped = dict(body.get("findings") or {}), []
while True:
data = _gzip({**body, "findings": findings})
data = _compress({**body, "findings": findings})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocker-ish: pack() only checks the compressed size against MAX_BUNDLE_BYTES (4.3 MB), but the service refuses a briefing over 32 MiB once decompressed (MAX_BRIEFING_BYTES in closed_functions/handler.py). xz gets roughly 10x here, so a briefing between ~32 and ~43 MiB gets sent whole, rejected, and retried on every sync.

Shouldn't we trim findings against 32 MiB of JSON first and then compress once? That would also take care of Codex's recompress-per-finding point.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c4978ea85

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +708 to +709
if not kept or e.fits is None or compressions == MAX_COMPRESSIONS:
raise

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep trimming findings when ratio retries are exhausted

When findings have different compression ratios, removing the largest entries can discard highly compressible data first and make each ratio estimate too optimistic; if all three attempts remain over the compressed limit, this branch raises even though kept still contains findings and the request without them may fit. That skips the entire briefing update rather than merely omitting additional findings, so the same oversized input is retried on subsequent syncs. Fall back to dropping more findings—at least trying the findings-free request—or use a bounded search that guarantees a fitting subset when one exists.

Useful? React with 👍 / 👎.

Svilen-Stefanov and others added 7 commits November 12, 2026 21:09
Large repos got no flows or merge risk: crate's bundle was 24.6 MB of gzip, over the service's
4.3 MB limit, because every fact went four times. 149,315 of its 149,316 analysis edges are call-graph
edges, nested relations repeat the top level, and the methods index and the graph's nodes are one
table.

- `side_document()` writes each commit as one document, `head.json` and `base.json`, in place of the
  two analyses and two call graphs. It holds a row per method or graph node, calls by caller with
  their lines, the component tree, and only the analysis edges the calls do not imply. The service's
  `bundle.expand_side` reads it back into every field a feature reads.
- `_compress` is xz -9e instead of gzip: its window spans the bundle, so the base side costs little
  next to a head that repeats most of it. The briefing request shares it.
- A changed file over 300 KB is sent at neither commit (the same limit as source).

On crate the bundle is 1.42 MB. Needs CodeBoarding/licensing-aws#56 deployed first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service reads every call already, so the analysis edges that repeat one were rebuilt on its side
from leaf membership and then corrected with hidden_edges. Without them crate's guide holds the same
content and its peak memory drops from 1.8 to 1.3 GB, so extra_edges is now just the analysis edges
that are no call, and the derivation and hidden_edges go.

call_graph() no longer builds what side_document dropped: `callable`, and each site's file and
column. It returns each call's lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The script calls the service's review-guide and architecture endpoints;
nothing about it is closed. scripts/remote_features.py replaces
scripts/closed_features.py, and the action step, problem_history's import
and the tests follow. The public input closed_features_source keeps its
name, since renaming it would break workflows that set it.

The rewrite uses descriptive names and short functions: side_document is
now a _Table of _Row objects plus _calls, _components and
_analysis_edges, and collect() reads changed files through
_changed_contents. The module docstring says what a request sends, why it
is packed as side documents under xz, and each size limit.

The side documents, collect() output and request body are byte-identical
to before on 42 cases, crate's head and base with its real call graph
among them, and call_graph() reads two real pickles the same.

One fix: a parent's members were left out when they equalled its leaves'
members, but the service rebuilds a parent from its direct children. A
parent holding fewer methods than a child read back with the child's
extra methods. It now compares with its children's members. No analysis
in the 42 cases hit it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
remote_features.py parsed the legacy analysis.json itself: methods_index,
each component's file_methods and every relation's all_edges. It now reads
an analysis only through the pinned release's parse_unified_analysis, the
same engine that unpickles the call graph beside it. The review analyses
both commits with that release, so both are always in the format it reads,
and a format change in Core needs no change here.

The check that a methods_index key is its file and qualified name goes:
the keys are built from Core's method entries.

InstalledCoreTests runs in the core-compatibility job against the pinned
release. tests/fixtures/analysis holds one analysis as Core 0.15.5 and Core
main (9cf50fb9, after #571) wrote it. Each release reads only its own, and
every fixture it reads must give the same side document.

Checked: on CodeBoarding's own analysis (two baseline syncs, 153 files,
22 components) and this repo's, the side document is byte-identical to the
one built from the raw document, and Core main reading the v1 rewrite gives
the same bytes as 0.15.5 reading the legacy original.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sending

The service refuses a request on two sizes: what is sent (4.3 MB of xz, the Function URL's limit) and
what it decompresses to (256 MiB of JSON for a feature, 32 MiB for a briefing). The Action checked only
the first, so a briefing of 32-43 MiB of JSON, which xz takes under 4.3 MB, went out whole, was refused,
and went out again on every sync.

_encode now enforces both in one step: it measures the JSON, which is cheap, then compresses once. The
limits sit together and name the service's constants they follow. The briefing leaves findings out
against the JSON limit first, largest first, counted per entry, and only recompresses when the
compressed size is still over, aimed by the ratio it just measured, at most 3 compressions in all. It
used to recompress once per dropped finding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guide and architecture each compressed their own body in parallel, nearly the same bytes twice,
and architecture compressed again after dropping source.json. At preset 9 | EXTREME each compressor
takes about 700 MB.

Both features can send the body without source, so it is compressed once and carries why source is
missing, which only architecture reads. Architecture's body with source is compressed only when
there is source; when it does not fit, the shared bytes stand in. A run compresses once without
source and at most twice with it, and never two at a time.

The JSON check needs no ratio guess: the 256 MiB limit is 62 times the 4.3 MB one, so it already
turns away whatever could only fit at better than 62:1, and no tighter cut-off holds for every
request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
load_analysis already parsed both sides with the pinned release's
parse_unified_analysis and rolled relations up with its
project_relations_to_level, but the diff then read members, file hashes and
method hashes from the raw document: file_methods as {file_path, methods}
groups and methods_index. Core #571's "v1" document has neither, so once the
Action pins a release with it, the review diff fails with "'str' object has
no attribute 'get'".

The diff now reads only what Core parsed: each component's file_methods,
the root's file index and Core's projected relations at every level. The
review analyses both commits with the pinned release, so both sides are
always in the format it reads, and a format change in Core needs no change
here.

The core-compatibility job rendered the committed .codeboarding/analysis.json,
which after a pin moves can still be in the format of the release before,
which the new one refuses. It now runs InstalledCoreTests: tests/fixtures/
analysis holds one analysis as Core 0.15.5 and Core main (9cf50fb9) wrote it,
each release reads only its own, and every fixture the pinned one reads must
give the same view.

Checked on CodeBoarding's own analysis (two baseline syncs, 22 components,
depth 1 and 2) and this repo's: output and summary are byte-identical to
main's script on the legacy documents with 0.15.5, and to that again on
their v1 rewrites with Core main, where main's script fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Svilen-Stefanov
Svilen-Stefanov force-pushed the feat/analysis-json-v1-reader branch from b026ec5 to 140f605 Compare November 12, 2026 20:28
@Svilen-Stefanov
Svilen-Stefanov changed the base branch from feat/analysis-json-v1-reader to main November 12, 2026 21:18
@Svilen-Stefanov
Svilen-Stefanov merged commit aa56857 into main Nov 12, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants