Uploads: replace a file in place, and say which file each version is - #683
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9cff43584a
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
All three Codex findings addressed in The generator fix (threads 1 and 2). Checked against
The TypeScript one was silent: The Kotlin one also falsified something I had written down. MIGRATING said that row was "unchanged — bare-array responses were already untyped here"; I had grepped the file after the change and read the result as the prior state. Corrected, and Kotlin now emits the models, so the conformance summarizer reads decoded values — which makes that case a real decode test there, as it already was in Swift. The source break (thread 3). MIGRATING now carries a per-SDK table for the new error code: TypeScript's
Copilot errored rather than reviewing and has been re-requested. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6be4404164
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 86cecb9784
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
|
CI note: the GHSA-5p4m-2wfm-xmqj (high) landed on
#685 already owns the fix ("Bump the js-yaml override past the !!omap advisory"), following the same override pattern as #624 for Everything else on |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 60eedb819a
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a72e79e558
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The reclassification is keyed on status, so it reaches every operation the spec gives a 507 — including CreateWebhook and UpdateWebhook, which have declared WebhookLimitError since long before this branch and were reporting it as a retryable api_error the whole time. Webhook and project callers therefore need the same new branch even though nothing about those endpoints changed, which is exactly the kind of thing a migration guide exists to say and mine did not. The list is now a table of all eight across the three limits, derived from openapi.json rather than recalled, with the query to re-derive it. That is also what makes the omission embarrassing rather than subtle: the six README tables this PR already updated say "file storage, projects, webhooks" — I knew the set and then wrote a shorter one here.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6dc3788312
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Third round on this class, so this time the sweep is the fix rather than the two references Codex pointed at. SECURITY.md said 249 operations and 41 non-idempotent POSTs; both move to 250 and 42. The rest of that paragraph checks out and is untouched — 125 GETs, 83 idempotent mutations, 52 PUTs, 24 DELETEs, 7 flagged POSTs, and the seven named are exactly the seven behavior-model flags. Verified rather than assumed, since the whole point of the last two rounds is that I patched what I was shown and not what was true. scripts/check-grouped-client-coverage's floor comment cited 249 as the real value. Not load-bearing — MIN_OPERATIONS is 200 and catches extraction collapse, not drift — but it is a current-value claim in prose and it was wrong. The sweep: every tracked file except MIGRATING (as-of history), generated output and lockfiles, scanned for 247/248/249 within ninety characters of "operation", "POST", "route" or "surface". Zero remaining.
|
@codex review |
|
Codex Review: Didn't find any major issues. What shall we delve into next? Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
#686) * Mark the operation count, so it stops being six hand-maintained copies 250 is one jq away from openapi.json and was restated in prose six times across four files. Adding one operation left five of them stale, and it took three rounds of review on #683 to reconcile — each round finding the copies the previous round had not been shown. @operation-count joins @api-version, @bc3-pin and @assertion-types in spec/doc-constants.json's existing machinery: `make sync-api-version` rewrites every marked span from the source, and `make doc-constants-check` fails on any that drifted. markerCounts pins six, so deleting one fails the gate rather than silencing it. The span must state the count as a BACKTICKED integer, and exactly one. Those sentences are full of other numbers — SECURITY.md names 125 GETs and 83 mutations beside the total — and a checker that read bare prose integers as the claim would fail on numbers it has no source for. Backticks are how the prose says "this is the derived constant", which is the device @bc3-pin already uses for the SHA. A line needing a second backticked integer cannot carry the marker, and the error says so. Counting is (path, HTTP method) pairs. Path items also carry parameters, summary and servers, so the verb list is explicit rather than "every key". Derived lazily. The gate's own fixtures are minimal OpenAPI documents with no .paths, and computing eagerly turned each into an error about a constant it never mentions. Six self-test cases, each shown to fail against the un-built feature: neutering the checker fails the three negative cases, neutering the rewriter fails the writer case, and counting every path-item key fails the positive control. The fixture's real operations live in the --openapi source with a five-operation decoy in the checkout, so a gate reading the wrong file reports 5 against a span that says 3. End-to-end: drifting all six spans to 999 flags six errors, and the writer restores all three files byte-identical. * The writer must refuse an ambiguous span, not rewrite every integer on it Codex found a real hole, and it was the dangerous kind: the writer corrupted data and the checker then certified the corruption. --write returns before the per-kind checkers run. So a blanket gsub over every backticked integer on the marked line ran FIRST, and the later check — comparing values that were by then all identical, because it deduplicated — went green over the damage. Reproduced on the real sentence before fixing. Backticking SECURITY.md's 125 GETs and 83 mutations, then `--write`: before: all `250` operations: the `125` GETs ... and `83` mutations after: all `250` operations: the `250` GETs ... and `250` mutations check: passes Two changes. The count is now found by one helper both the checker and the writer call, so they cannot disagree about which integer is the claim; and that helper counts OCCURRENCES rather than distinct values, because two spans both reading `250` today would both be rewritten the day the count moves and only one of them is the count. An ambiguous span is now left exactly as written. That is deliberate rather than a silent skip: untouched, it fails the next --check with a message naming the integers it found and what to do about them. The self-test drives --write over an ambiguous span, asserts the other integer survived, and then runs --check in the same directory to assert the span is still rejected. Restoring the blanket gsub fails it.
…s canary (#698) * Sync all six SDK-version lockfiles on bump, and make release's preflight agree bump-version.sh synced four of the six lockfiles that record the SDK's own version through a path dependency. The conformance Ruby and Python runner lockfiles were missed, and both are gitignored — so on any machine that had run conformance at the old version, the first post-bump make check re-resolved them mid-check and assert-lockfiles-unchanged correctly failed (#671). Sync both in the bump, mirroring the conformance TypeScript step, and extend make release's preflight to assert them too — guarded on existence, since a fresh clone legitimately lacks them. The preflight and make check now agree on what "in sync" means. Closes #671 * Record the upload-versions live canary as done Ran 2026-08-11 against production (account 2914079, Coworker QA Sandbox vault) via a throwaway Go program built on this repo's go/ module. Every deferred assertion from the #683 plan passed: stable id/URL across CreateVersion, typed ListVersions decode with exactly one Current entry, byte-compared downloads of both the original and the replacement, and description carry-forward.
SPEC §19's Test Categories table and Appendix D each claim to account for every fixture under conformance/tests/, and both had drifted. The last three fixture-adding commits missed the convention in three different ways: dee221c (#601) added documents_write.json and updated neither table; b238e5e (#683) added uploads_write.json with four Appendix D rows and no §19 row; #726 added search.json's §19 row and missed Appendix D. Two half-applications in opposite directions is not carelessness — CONTRIBUTING.md tells contributors to add conformance tests and mentions neither table. Each cell is derived from the fixture's own description citations, which is the convention the existing rows follow. documents_write.json cites "SPEC 18 body compaction" and "SPEC 18 rule 6", and §5's Documents subsection already back-references it; uploads_write.json cites "SPEC §18", "SPEC.md §5" and "SPEC §6 step 11", the same four attributions its Appendix D rows already spell out. Also moves the search row into its sorted position, where #726 misfiled it between retry and schedule-entries-write. Claude-Session: https://claude.ai/code/session_01VyZUi8zkRfhatnBYoS4eyn
…them (#740) * Account for every conformance fixture in both SPEC rosters SPEC §19's Test Categories table and Appendix D each claim to account for every fixture under conformance/tests/, and both had drifted. The last three fixture-adding commits missed the convention in three different ways: dee221c (#601) added documents_write.json and updated neither table; b238e5e (#683) added uploads_write.json with four Appendix D rows and no §19 row; #726 added search.json's §19 row and missed Appendix D. Two half-applications in opposite directions is not carelessness — CONTRIBUTING.md tells contributors to add conformance tests and mentions neither table. Each cell is derived from the fixture's own description citations, which is the convention the existing rows follow. documents_write.json cites "SPEC 18 body compaction" and "SPEC 18 rule 6", and §5's Documents subsection already back-references it; uploads_write.json cites "SPEC §18", "SPEC.md §5" and "SPEC §6 step 11", the same four attributions its Appendix D rows already spell out. Also moves the search row into its sorted position, where #726 misfiled it between retry and schedule-entries-write. Claude-Session: https://claude.ai/code/session_01VyZUi8zkRfhatnBYoS4eyn * Gate both conformance-fixture rosters against git ls-files `sync-doc-constants.rb` already does table-completeness checking: @assertion-types wraps SPEC §19's assertion-type table in a block marker and set-compares it against conformance/schema.json. The two fixture rosters are the same shape one level out — a table that claims to account for every fixture under conformance/tests/, with nothing checking it — so they become two more block kinds rather than a new script and a new CI step. `make doc-constants-check` already runs the gate and its self-test, and is already in check-targets and spec-gates. The source is `git ls-files conformance/tests/*.json`, not Dir.glob, for tracked_markdown's reason: an untracked scratch fixture must not fail a developer's build. Direct children only — git's pathspec `*` matches across `/`, and a nested fixture is discovered by no runner, so demanding a roster row for it would be documenting a claim that is not true. That scope is also how SPEC §23's carve-out is honored: conformance/oauth/, oauth-token/ and event-feed*/ are documented at their own section and directory. The two invariants differ because the artifacts differ. §19's table is a bijection, so all of it is asserted: one row per fixture, both directions, and category slug == basename with `_` as `-` (verified across all 22 rows). Appendix D's rows are curated summaries that deliberately bundle several cases — uploads_write.json legitimately has four — so it gets coverage only, and a self-test case pins that difference by asserting several rows for one fixture still passes. Both tables also reject a row whose attribution cell is blank, and a `§N` reference that resolves to no `## §N.` heading — the latter catching a reference that resolved when written and stopped resolving when a section was renumbered, which a reviewer of the same PR cannot see. A row with no section reference at all is still accepted, because rejecting it needs a carve-out for live-my-surface.json's external-governance attribution and the carve-out list is the part that grows. Neither table is writable: --write only ever touched line spans, and a row here carries an owning-section attribution or a case summary only the fixture's author can make. Both checks reject SPEC.md as it stood before the preceding commit: SPEC.md:2110-2131: conformance/tests holds 22 tracked fixture(s), the table categorises 20; missing: `documents_write.json`, `uploads_write.json`. SPEC.md:3379-3458: no row maps these tracked fixtures to a primary section: `documents_write.json`, `search.json`. Claude-Session: https://claude.ai/code/session_01VyZUi8zkRfhatnBYoS4eyn * Keep block bodies under the pin scan, and reject colliding category slugs Two review findings on the roster gate, both real interactions rather than Markdown-spelling edge cases. Block span bodies were dropped from the prose pool along with line spans, but the two are not alike. The writer rewrites line spans only, and the block checkers read nothing but the `|` rows, so an ordinary sentence parked inside a roster or assertion-types block survives both untouched. Excluding the whole body let "verified against <current pin>" sit there with no marker and no grant — invisible to check_unmarked_pin and silently stale at the next repin, which is the exact claim class this gate exists to catch, hidden by the gate's own span bookkeeping. Only line spans leave the pool now. The §19 categories table tallied FILES, which catches one fixture on two rows but not two fixtures deriving one category. `_` and `-` collapse to the same slug, so `foo_bar.json` and `foo-bar.json` each satisfy the per-row slug rule while the table stops being the bijection its heading asserts. Now tallied on the DERIVED slug — a row whose category cell is simply wrong is already reported and still reaches that tally, so grouping by the declared cell would both miss real collisions and invent false ones. Appendix D is unaffected: it has no category column and deliberately allows many rows per fixture. Both self-test cases were shown to fail against the un-fixed gate first — reverting each fix in turn leaves exactly its own case reporting "expected FAILURE, gate exited 0". * Stop SPEC promising a gate this PR removed, and correct a comment inviting deletion Codex raised the first as a P1 and Copilot as a suppressed comment; both were right, and it is the defect class this PR family exists to prevent — prose claiming coverage CI cannot provide. SPEC §19 said `make check-fixture-execution` (#602) "is what detects it now". That gate was the source-text parser split out of this PR to conformance-skips-parser-archive; the prose describing it stayed behind. There is no such script and no such target — `make -n check-fixture-execution` exits "No rule to make target" — so the paragraph promised all-six detection that does not exist, while #602 is still open. Replaced with what is actually true: each runner's case census (#742) catches a case executed by no runner for a MECHANICAL reason, and explicitly does not catch the deliberate all-six exclusion this section describes, because each census counts its own skip and stays green. The roster below it is restated rather than derived, and nothing checks it (#736) — which is why #736 waits for #602's cross-runner manifest instead of being fixed on its own. Separately, roster_vacuity's comment claimed the guard "buys no coverage". That is true only when ONE side is empty. When BOTH are, `missing` and `extra` are both empty, the comparison is trivially satisfied, and this guard is the only thing refusing the vacuous pass — a committed self-test case covers exactly that. The comment as written invited deleting a live guard on the strength of reasoning that applies to a different case. * Require exactly three cells per roster row, and correct a self-falsifying claim Copilot raised the cell count three times across two rounds; taking it, because it asks for something different from the Markdown-spelling findings declined alongside it. Those asked the splitter to UNDERSTAND more Markdown — separator widths, backslash parity. This asks it to REFUSE what it does not understand, which is the direction this file already argues for: "a row the parser cannot see is a row it silently vouches for." And it closes the pipe class as a class rather than one spelling at a time — however a stray pipe was written, the cell count is wrong and the row fails loudly instead of being mis-parsed quietly. The consequence was real, not cosmetic. A raw pipe in an attribution shifts the real section into a fourth cell and leaves the fragment before it in cells[2], where non-`§` attributions are legitimately allowed — so the gate validated the wrong cell and a `§99` in the actual section position was never checked, on a gate whose whole claim is that it validates every section reference. Both tables had it; Appendix D's free-form summaries are the likeliest place for someone to write `supports A | B`. Self-test cases added for both tables and shown to fail against `< 3` first. CONTRIBUTING.md separately claimed the checklist item "is the only place this convention was written down" — false the moment this PR also stated it in SPEC §19 and the gate. Rephrased as the historical absence it describes.
…st honesty Four small fixes ahead of v0.10.0: - .github/release.yml excluded the label `github-actions`, which does not exist in this repo; the real label is `github_actions` (28 merged PRs carry it, none carry the hyphen form). Latent today because the author exclusion catches dependabot, but a human-authored CI PR would have leaked into "Other Changes". - installer-smoke.yml gains a `recover` job: on a fully green run it closes any open "Installer canary failure" issue, using the same exact-title lookup as the notify job. Without it a single blip leaves a permanently open issue — #644 sat open ten days across ten green runs. - API-COVERAGE.md's header had drifted three SDK bumps behind the pin: it claimed v0.12.0 (actual: v0.15.0), still carried the uploads-versions gap that closed with v0.14.0's typed ListVersions (basecamp/basecamp-sdk#683 — the `uploads` row already documented the shipped command), and said the field-keyed 422 fix was "past this pin" when #541 landed inside it at v0.13.0. Summary is now 184/184 with the Blocked status retained at zero. The endpoint-count reconciliation the file defers stays deferred. - check-cli-surface-diff.sh's header said to clear .surface-breaking after each release; five months and several releases in, nobody ever has, RELEASING.md never mentions it, and nothing depends on it. The comment now describes the allowlist as what it is: cumulative.
Absorbs basecamp/bc3#12555 and #12565. Closes #649. Closes the write side of
spec/api-gaps/upload-new-version.md, open since 2026-07-22, and unblocks basecamp-cli#404.The write
POST /uploads/{id}/versions.jsonreplaces an upload's file in place. The recording keeps its id, its URL and its comments; the previous file becomes a past version.CreateUploadcannot do this — it mints a new recording with a new id and a new URL, so every published link rots on each release.CreateUploadVersionmodelsattachable_sgid,base_name,description,notifyandsubscriptions.notifyandsubscriptionsare documented contracts as of #12565, not guesses.Subscribers#notify_paramdefaults to"custom", so an audience arrives through two doors:notifynaming a mode, or a baresubscriptionsarray taken as custom. The plan for this work assumed neither was caller-selectable; reading the concern showed the second door is live and that bc3's own web tests already rely on it.visible_to_clientsis deliberately absent — #12565 removed it from the endpoint's reachable surface, because it never set the recording's visibility, only widened the notification audience, and could announce a client-invisible file to a project's clients.The read — this is the half that fixes #649
ListUploadVersionsOutputdeclareduploads: UploadList. The endpoint returns events: 11 ofUpload's 14@requiredmembers are absent from every response. That typed lie is why the CLI's versions command and the MCP server'slist_upload_versionsrender blank fields — the SDK was decoding an event payload into a struct whose required fields it does not carry.It now returns
UploadVersion/UploadVersionFile, built from_version.json.jbuilderoverrecordings/events/_event.json.jbuilderrather than from theUploadshape it was pretending to be. New shapes rather thanEventListplus a member, for the reason bc3's own commit gives for using a purpose-built partial: widening the shared one "would leak upload fields onto todo, message, and card events". Recorded in SPEC §10 as the second worked example of One Renderer, One Schema.The 507, which turned out to be four operations wide and then six
StorageLimitErrordeclares the 507ensure_account_can_upload_fileshas always been able to raise. That guard fronts four modelled operations, so all four take it — absorbing the contract for the new one alone would leave three declaring a status they can return and don't model.The larger find is the classification. SPEC §6 had no 507 step, so a 507 fell through
status >= 500and surfaced asapi_errorwithretryable: true— a plan limit no retry can satisfy, reported as a transient server error. §6 now maps 507 tolimit_exceeded, non-retryable, ordered ahead of the 5xx catch-all, with exit code 10 in all six SDKs.That also fixes
ProjectLimitError, which #679 added days ago. Five SDKs carried tests pinning the old behaviour, each commented "No SDK gives 507 a named class". Python's went further and recorded a genuine cross-SDK divergence — its fallback arm producedretryable=Falsewhile the other five marked every unclassified 5xx retryable, "asserted here as-is rather than fixed in passing". All six now agree onFalse, andFalseis what the spec says rather than an accident of which arm caught the status.Nothing retried a 507 in practice — no
retryOnlist names it — so this changes what the caller is told, not what the client does.Breaking
Two entries in
MIGRATING.md. TheListUploadVersionsretype is compiler-silent in Ruby, Python and Kotlin: the type does not change, only which keys are actually present. Go'sUpdateUploadRequest.Description→*stringthe compiler does catch.BaseNamestays a plainstringon both request types, and says why in its doc comment:Upload#base_name=guards onnew_base_name.present?, so""and absent are the same server write and there is no third state a pointer could express.Verification
makepasses clean. 250 operations.Every new assertion was run against un-fixed code first:
TestUpdateUploadRequest_DescriptionIsTriState→Description: the clear spelling must reach the wireTestUploadVersionFromGenerated_DetailsSurvives→a present but empty details object must survive as non-nilexpected 'api_error' to be 'limit_exceeded'conformance/tests/uploads_write.json— 7 cases, all six runners green. Named for the surface, not the operation, because it has to cover both writes:UpdateUploadlands on the same serialized ActionText attribute and had its presence semantics pinned nowhere. The uploads surface had zerorequestBodyAbsentassertions before this; each rides with arequestBodypresence assertion in the same case, since arequestBodyAbsentalone is satisfied by an empty body.The shared fixture is built from the renderer, and its mock bodies come from
spec/fixtures/uploads/get.json— Swift's strict decoder rejected hand-rolled ones for missingbucket, which is a better outcome than passing.Notes for review
registry:waivers Repin bc3 to 7fe1c63ab3 and absorb project archive/unarchive #679 parked for these routes are deleted here — absorption is what removes them. Three gap entries this branch wrote were dropped as duplicates of Repin bc3 to 7fe1c63ab3 and absorb project archive/unarchive #679's better ones.resource :authorizationto bc3's routes, so theout_of_scopewaiver's "served from Launchpad" justification had decayed. Repin bc3 to 7fe1c63ab3 and absorb project archive/unarchive #679'sbc5-authorization-document-shape.mdcovers it, and splits the OAuth work more carefully than this branch had.upload-create-subscriptions.md.CreateUploadInputhas always declared asubscriptionsmember thatUploadsController#createnever reads — the same class of lie as ListUploadVersions declares UploadList but BC3 returns recording events — 11 of 14 required members absent #649, in the structure next to it. Not fixed here; removing an input member is breaking and belongs in a change whose title says so.TestUpdateUploadRequest_HasNoFileReplacementFieldstays. bc3 chose a dedicated route rather than widening the PUT, so it now pins a design choice rather than a missing feature.Still open
upload-new-version.md, and basecamp-cli#404's command.Summary by cubic
Adds
CreateUploadVersionto replace an upload’s file in place and retypes versions reads to returnUploadVersionwith the file each event recorded. Also reclassifies HTTP 507 to non‑retryablelimit_exceededacross SDKs and sweeps docs to the correct 250‑operation count.New Features
POST /uploads/{id}/versions.jsonviaCreateUploadVersion; modelsattachable_sgid,base_name,description,notify,subscriptions.""clears, value sets.UploadVersionwithuploaddetails andcurrent.currentis positional and exactly one per non‑empty response; it does not mean “the file the upload’sdownload_urlserves” after a metadata‑only update.Migration
listVersionsnow returnsUploadVersion(notUpload) in TypeScript, Swift, and Kotlin; update callers. Ruby/Python decode unchanged at runtime.limit_exceeded(exit code 10), non‑retryable. Update exhaustive handling in TypeScript, Swift, Kotlin, and Python; applies to eight ops:CreateUpload,CreateUploadVersion,CreateDocument,CreateCloudFile,CreateProject,UnarchiveProject,CreateWebhook,UpdateWebhook.UploadVersionandCreateVersionUploadRequestare exported fromindex.ts; update imports if used.UpdateUploadRequest.Descriptionis now*string; 507 maps tolimit_exceededon generated and raw HTTP paths.upload.current === false; do not filter onaction === "blob_changed".Written for commit 1797145. Summary will update on new commits.