Skip to content

Commit 39abe1d

Browse files
os-elon-muskclaude
andauthored
fix(pm): a size refusal is a refusal — post-stamped stops reporting a real 422 as a missing route (#18934)
Fixes #18843 Clause-②: no `post-stamped.mjs` reported a real HTTP 422 from the platform as exit 3 `EXIT_PREREQUISITE_NOT_MET` — 「no route, no act at all」 — and prescribed a transport remedy. This gives the size refusal an exit class of its own, whose text carries the bytes SENT, the measured cap for THAT surface, and the remedy that works; and it homes the comment cap beside `ISSUE_BODY_LIMIT`, which the card says is the same card. ## The fixture, verbatim, and where it came from Built from the record on probe objectstack#18826 — comment id **5722505167**, written 2026-09-17T23:19:18Z. ⛔ No new oversized write was made anywhere to reproduce this. Captured there from `POST /issues/{n}/comments` at 262,145 bytes: ```json {"resource":"IssueComment","code":"unprocessable","field":"data", "message":"Body is too long (maximum is 65536 characters)"} ``` ## The two readings the dispatch carried as unmeasured, now measured 1. **The 422 message is NOT the same string on the two endpoints.** The record is explicit: the create side answers `Body is too long (maximum is 65536 characters)`, and `PATCH /issues/comments/{id}` answers the same sentence with a lower-case `body`; `POST /issues` says it twice. So the trigger is case-insensitive rather than an equality — an equality pinned to the create-side capital would have left the update side at exit 3. Pinned in both spellings, and the ablation below proves the flag is load-bearing. ⚠️ One correction to the dispatch's framing: `PATCH /issues/comments/{id}` is not a surface this tool ever writes. Its two write surfaces are `POST /issues/{n}/comments` (`--comment`) and `PATCH /issues/{n}` (`--body`). Both caps are declared; the update-side spelling still matters, because it is the second measured spelling of the sentence the trigger keys on. 2. **The two caps live as two constants, beside each other in `check-half-states.mjs`.** `COMMENT_BODY_LIMIT` is declared after `MARKDOWN_BODY_BUDGET`, with its own bisection provenance. It is homed there rather than in `post-stamped.mjs` on a measured ground: five workflow comments (`cross-repo-issue-closer.yml`, `merge-queue-triage.yml`, `scheduled-full-run-card.yml`, `test-nightly-tiers.yml`) already send a reader to `ISSUE_BODY_LIMIT` in that file for the number and warn against re-deriving 65,536 — the folklore grows back wherever one of two caps is kept somewhere else. ⛔ Two constants and not one shared number: two surfaces measured independently that agree is not one measurement, and the day the platform moves one a shared number would lie about the other. ## The exit register, before and after | code | before | after | |---:|---|---| | 0 | written, and everything sent is on the platform | unchanged | | 1 | usage | unchanged | | 2 | the body broke the stamp contract | unchanged | | 3 | PREREQUISITE NOT MET — no route, no token, no act at all | unchanged, and it now prints what the platform said | | 4 | written, and the platform did NOT store it | unchanged | | 5 | — | **TOO LARGE — the platform ANSWERED and refused the body for its size. Nothing written; shorten or split it. ⛔ Not a route problem.** | `EXIT_TOO_LARGE = 5` is declared in `post-stamped.mjs` beside `EXIT_NOT_STORED`. The usage text, the not-stored text and the prerequisite text each name it, so a reader of a 3 or a 4 learns the fifth code exists. ## The reader test A seat that sends a 300 KB comment body now reads: **exit 5, 307,200 byte(s) sent, cap 262,144 bytes for a comment, 45,056 byte(s) over it, shorten or split the body** — and ⛔ not a word about routes or tokens. Verbatim from the `--self-test` fixture: ```text post-stamped: TOO LARGE — the platform ANSWERED this write and REFUSED it: the body is over the cap for this surface. NOTHING WAS WRITTEN. Sent to #18843: 307,200 byte(s) as a comment (`POST /issues/{n}/comments`). The measured cap for a comment is 262,144 bytes — UTF-8 BYTES, bisected on objectstack#18826, one byte either side. This body is 45,056 byte(s) over it. The platform said: Validation Failed · Body is too long (maximum is 65536 characters) ⛔ Its number is FALSE in unit and value — the same fleet measured a 262,144-BYTE comment stored, four times what that sentence claims as a maximum. ``` ## What the class deliberately does NOT take - **Not every 422.** The issues listing refuses deep pagination — page 99 stores, page 100 answers 422 (measured 2026-08-31, recorded in `check-half-states.mjs`) — and that refusal is about a cursor. It keeps its 3, pinned as a control, as do the same sentence on a 403, a 500 and a 200. - **Not the platform's number.** The trigger stops before `maximum is`, so keying on a false clause cannot un-classify the refusal the day GitHub corrects its own text; a text carrying only that clause is pinned NOT to classify. - **Not the read path.** A read sends no body, so the `--body` pre-read keeps its PREREQUISITE reading unchanged. - **Not a widened `--body` story.** Over the cap, `PATCH /issues/{n}` refuses SILENTLY (200, old body kept, #18793), which is exit 4's population; only a 422 the platform actually answers reaches 5. ## A refusal the cap does not explain If the platform ever refuses a body at or under the measured cap, the text says the refusal CONTRADICTS the reading and asks for a re-measurement rather than printing an overage of zero — `over` is `null`, never 0. The ordinary over-cap text is pinned NOT to carry that sentence. ## Self-test counts | battery ledger | before | after | |---|---:|---:| | `post-stamped --self-test` cases | 349 | **402** | | `post-stamped` batteries (floor) | 13 | **14** | | new battery `the size refusal: …` (registered/floor) | — | **53 / 53** | | `check-half-states --self-test` cases | 4,963 | **4,978** | | battery `ISSUE_BODY_LIMIT measured cap` (registered/floor) | 40 / 37 | **55 / 52** | The check-half-states "before" is the merge base `d5ccfb4e3`: 4,978 minus the 15 cases this PR registers. Two independent readings agree on it — the dispatch recorded 4,963 at PR #18911's head, and this branch measures 4,921 at its own branch point `625db0e85`, which is before #18911 landed. Both floors RISE by exactly the number of cases added; neither is held flat. ## Ablations — each mutation proven on disk, each restore proven against HEAD All four through `scripts/ablation-replace.mjs`, which asserts the anchor count fell, the blob hash changed, and after restore that the blob equals HEAD's and `git diff HEAD` is empty. | mutation | expected | observed | |---|---|---| | drop the `i` flag from `BODY_TOO_LONG_RE` | red | **19 of 402 cases fail by name**; the lower-case pin stays green, so the ablation discriminates | | put `ROUTE_REMEDY` in the size text's `Fix:` line | red | **exactly the 2 held-apart pins fail** | | `COMMENT_BODY_LIMIT` 262144 → 65536 | red | **5 of 4,978 cases fail by name** | | delete `UTF-8 BYTES` from the comment cap's docblock | red | **exactly 1 pin fails** — and the sibling docblock also contains that phrase, so a mis-pointed extractor would have stayed green | ⭐ **The first ablation found a defect in this PR's own first draft.** It came back GREEN: the classifier was rebuilding the regex from `.source` with the flags retyped at the call site, so the exported constant documented a rule it did not carry and every case-insensitivity pin was vacuous. Fixed in `36ba41a64`, after which the same ablation turns 19 cases red. A second pass (`290acfad0`) made those pins fail by NAME rather than crash on a TypeError before the verdict machinery ran. ## Gates Derived in the worktree with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at HEAD `0e5e054d3`: **39 families, all 39 run, all exit 0**, exit codes captured by redirect-then-`$?`. `--ran` reconciliation: 39 derived, 39 run, 0 UNRUN. The first derivation (before the merge, one path) gave 29; adding `check-half-states.mjs` grew it by 10 and those were run too. Each edited script's own `--self-test` is in that set (`check:pm-post-stamped`, `check:pm-half-states`). `git grep` of both filenames under tests finds no separate suite — the `--self-test` IS the suite. **ESLint was not narrowed at all.** The repo-wide run is the one that was taken: `pnpm lint` (= `eslint . --no-inline-config`, this repo's only style authority) **exit 0** at HEAD `0e5e054d3`. The targeted run over the two changed files is there too — 2 files, 0 errors, 0 warnings — but it is not what this PR rests on. ## Acceptance notes - **Exit 3 still hosts other refusals the platform ANSWERED.** The deep pagination 422 is the measured one, and its remedy text is a route remedy just as wrongly. ⛔ Not filed: it is unreachable through this tool (a comment list would need 10,000 comments to reach page 100), so it is an observation, not a reproducible defect. - **`rest()` now surfaces the platform's sentence on every non-2xx**, which is the same defect family (the tool discarding the answer it was given) and is mechanically part of this change; it is not a separate fix. --- _Generated by [Claude Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9ee8e35 commit 39abe1d

2 files changed

Lines changed: 514 additions & 10 deletions

File tree

‎scripts/pm/check-half-states.mjs‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17256,6 +17256,46 @@ export const OUTPUT_FORMATS = ['plain', 'markdown'];
1725617256
export const ISSUE_BODY_LIMIT = 262144;
1725717257
export const MARKDOWN_BODY_BUDGET = 60000;
1725817258

17259+
/**
17260+
* The largest COMMENT body the platform stores, in the same unit: UTF-8 BYTES.
17261+
*
17262+
* The sibling surface, bisected separately. `post-stamped.mjs` imports this to
17263+
* name the cap in a size refusal, and it is declared HERE rather than there
17264+
* because this file is already where this fleet's measured body caps live: five
17265+
* workflow comments send a reader to `ISSUE_BODY_LIMIT` in this file for the
17266+
* number, and the 65,536 folklore this pair replaces is the kind that grows back
17267+
* wherever one of two caps is kept somewhere else.
17268+
*
17269+
* Bisected on a throwaway probe opened for it (objectstack#18826, 2026-09-17),
17270+
* twelve measurement writes, every one read back byte-exact:
17271+
*
17272+
* 262,144 bytes STORED (read-back class `identical`)
17273+
* 262,145 bytes REFUSED (HTTP 422, nothing written)
17274+
*
17275+
* ⭐ The same value as `ISSUE_BODY_LIMIT`, reached by a SECOND, independent
17276+
* bisection — two surfaces, two brackets, one number: 256 KiB exactly. ⛔ And
17277+
* therefore two constants and not one: two measurements that agree is not one
17278+
* measurement, and the day the platform moves one surface a shared number would
17279+
* lie about the other.
17280+
*
17281+
* ⛔ The unit is measured here too. A comment of 262,145 bytes carrying only
17282+
* 222,145 characters (a run of 3-byte U+4E2D plus ASCII padding) was REFUSED —
17283+
* a cap counted in characters, or in UTF-16 code units, would have taken it with
17284+
* 40,000 to spare — and the same multi-byte shape at 262,144 bytes / 222,144
17285+
* characters STORED.
17286+
*
17287+
* ⚠️ What the two surfaces do NOT share is the SHAPE of the refusal. Over the
17288+
* cap, a comment create answers a real HTTP 422 and writes nothing, while an
17289+
* issue-body update refuses SILENTLY — 200, the old body kept, nothing reported
17290+
* (#18793). `post-stamped.mjs` carries one exit code for each.
17291+
*
17292+
* ⛔ And the platform's own 422 text says `maximum is 65536 characters`, which
17293+
* is false in unit AND value: write 3 of that probe stored a 65,537-character
17294+
* comment and write 4 a 262,144-byte one, four times the claimed maximum. ⛔
17295+
* Nothing here is derived from that sentence.
17296+
*/
17297+
export const COMMENT_BODY_LIMIT = 262144;
17298+
1725917299
/**
1726017300
* A body's size as the PLATFORM counts it: UTF-8 bytes.
1726117301
*
@@ -24375,7 +24415,7 @@ export const SELF_TEST_BATTERIES = Object.freeze({
2437524415
// docblock — unit, both sides of the bracket, the date and the probe issue —
2437624416
// because a number with no provenance is what got re-derived from memory the
2437724417
// first time.
24378-
'ISSUE_BODY_LIMIT measured cap': 37,
24418+
'ISSUE_BODY_LIMIT measured cap': 52,
2437924419
});
2438024420

2438124421
/** The floor on the ROSTER itself — how many batteries must be declared at all. */
@@ -28559,6 +28599,32 @@ async function selfTest() {
2855928599
b(BATTERY18664, '#18664 cap: ⛔ the retired 65,536 is gone', ISSUE_BODY_LIMIT === 65536, false);
2856028600
b(BATTERY18664, '#18664 cap: …and the measured cap is four times it', ISSUE_BODY_LIMIT === 65536 * 4, true);
2856128601

28602+
// #18843: the COMMENT surface's cap, declared beside its sibling. Pinned in
28603+
// THIS battery because what the battery floors is a measurement and its unit,
28604+
// and the sibling is the same measurement on the other surface — ⛔ not in a
28605+
// battery of its own, which would leave the name-roster pin below describing
28606+
// a ledger it no longer covers.
28607+
const commentCapDocblock = (() => {
28608+
const at = selfSource.indexOf('\nexport const COMMENT_BODY_LIMIT =');
28609+
const open = at < 0 ? -1 : selfSource.lastIndexOf('/**', at);
28610+
return at < 0 || open < 0 ? '' : selfSource.slice(open, at);
28611+
})();
28612+
b(BATTERY18664, '#18843 provenance: ⭐ the extractor really found the comment cap\'s own docblock', commentCapDocblock.startsWith('/**') && commentCapDocblock.endsWith('*/'), true);
28613+
b(BATTERY18664, '#18843 provenance: ⛔ …and not the whole file, which would satisfy every pin below with its own text', commentCapDocblock.includes('export const'), false);
28614+
b(BATTERY18664, '#18843 provenance: the docblock names the UNIT', commentCapDocblock.includes('UTF-8 BYTES'), true);
28615+
b(BATTERY18664, '#18843 provenance: …the landed side of the bracket', commentCapDocblock.includes('262,144 bytes STORED'), true);
28616+
b(BATTERY18664, '#18843 provenance: …the refused side, one byte up', commentCapDocblock.includes('262,145 bytes REFUSED'), true);
28617+
b(BATTERY18664, '#18843 provenance: …the date it was taken', commentCapDocblock.includes('2026-09-17'), true);
28618+
b(BATTERY18664, '#18843 provenance: …the issue it was measured on', commentCapDocblock.includes('objectstack#18826'), true);
28619+
b(BATTERY18664, '#18843 provenance: …and that THIS surface refuses LOUDLY, which the issue body does not', commentCapDocblock.includes('HTTP 422'), true);
28620+
b(BATTERY18664, '#18843 cap: the comment cap is the bisected value', COMMENT_BODY_LIMIT, 262144);
28621+
b(BATTERY18664, '#18843 cap: …which is 256 KiB exactly, checkable by hand', COMMENT_BODY_LIMIT === 256 * 1024, true);
28622+
b(BATTERY18664, '#18843 cap: ⛔ the retired 65,536 is gone here too', COMMENT_BODY_LIMIT === 65536, false);
28623+
b(BATTERY18664, '#18843 cap: …and the measured cap is four times it', COMMENT_BODY_LIMIT === 65536 * 4, true);
28624+
b(BATTERY18664, '#18843 pair: ⭐ the two surfaces agree — two bisections, one number', COMMENT_BODY_LIMIT === ISSUE_BODY_LIMIT, true);
28625+
b(BATTERY18664, '#18843 pair: ⛔ …and each stands on its OWN probe, so neither provenance borrows the other\'s', capDocblock.includes('objectstack#18793') && capDocblock.includes('objectstack#18826') === false, true);
28626+
b(BATTERY18664, '#18843 pair: ⛔ …the comment one likewise cites only the probe that measured it', commentCapDocblock.includes('objectstack#18793') === false, true);
28627+
2856228628
// The unit, at the primitive the guard is built on.
2856328629
b(BATTERY18664, '#18664 unit: bodyBytes counts UTF-8 bytes', bodyBytes('\u4e2d'), 3);
2856428630
b(BATTERY18664, '#18664 unit: …where `.length` calls the same character one', '\u4e2d'.length, 1);

0 commit comments

Comments
 (0)