Repository navigation
Commit 8904880
fix(pm): declare ISSUE_BODY_LIMIT from a measurement, in the unit the platform refuses in (#18796)
Fixes #18664
Clause-②: no
## The defect
`scripts/pm/check-half-states.mjs` declared `export const
ISSUE_BODY_LIMIT = 65536;` with a docblock asserting "GitHub's hard cap
on an issue body ... A body that exceeds the cap is REJECTED by the
API", and no provenance at all: no unit, no reading, no date. It was
false in both directions at once. Bodies four times larger store fine,
so the number is too strict where it is used to bound a report; and a
real refusal boundary sits above it that nothing was watching, and
crossing it is SILENT — the platform keeps the OLD body, answers 200 and
reports nothing (which is precisely why `post-stamped.mjs` grew
`EXIT_NOT_STORED`). One constant, over-strict where it cuts and blind
where the refusal lives.
## Premise re-derived on `main` before any edit
- The filing card's first artefact has MOVED since it was measured.
objectstack#6015 was read by the triage seat at 150,507 chars / 257,945
bytes; fetched again for this PR at 2026-09-17T21:03Z it had been
compacted to **72,754 chars / 125,561 bytes** (`updated_at`
2026-09-17T20:04:42Z). The card's own numbers are therefore quoted here
as the card's, not re-taken. The falsification survives the compaction
anyway and is re-taken here: 72,754 chars / 125,561 bytes is past 65,536
in EITHER unit, so no reading of the old constant survives.
- The constant and its docblock were confirmed on the tip this branch
was cut from (`6de7a2d6e6`), at `:17148` / `:17149`, with the renderer's
cut at `:19420` and the pin at `:28367`. `SEAT_BODY_SOFT_LIMIT` is at
`:2037` (the card's `:1802` is stale); it and H6 were READ and are
untouched, as the card directs.
- One nuance the card's causal sentence does not carry, stated so the
next reader is not surprised: what trims the report today is
`MARKDOWN_BODY_BUDGET` at `:19420`, not `ISSUE_BODY_LIMIT` — the cap is
what the budget was chosen to sit under, and it appears in the self-test
pins. The defect is the same; the mechanism has one step in it.
## The measurement
A throwaway issue was opened for it — objectstack#18793, never a seat
post and never another card — and its body rewritten 17 times with
filler of chosen byte sizes through `scripts/pm/post-stamped.mjs
--body=18793`, each write read back byte-exact. Budget set for this: 18
writes (a dozen was the guide; the exact bracket cost 15, the unit
question 2, the record 1). Exit 0 with class `identical` = every byte
sent is stored; exit 4 (`EXIT_NOT_STORED`) = the platform kept the old
body.
| # | size sent (bytes) | chars | exit | class | verdict |
|---:|---:|---:|---:|---|---|
| 1 | 257,945 | 257,945 | 0 | identical | landed |
| 2 | 263,533 | 263,533 | 4 | not stored | refused |
| 3 | 260,739 | 260,739 | 0 | identical | landed |
| 4 | 262,136 | 262,136 | 0 | identical | landed |
| 5 | 262,834 | 262,834 | 4 | not stored | refused |
| 6 | 262,485 | 262,485 | 4 | not stored | refused |
| 7 | 262,310 | 262,310 | 4 | not stored | refused |
| 8 | 262,223 | 262,223 | 4 | not stored | refused |
| 9 | 262,179 | 262,179 | 4 | not stored | refused |
| 10 | 262,157 | 262,157 | 4 | not stored | refused |
| 11 | 262,146 | 262,146 | 4 | not stored | refused |
| 12 | 262,141 | 262,141 | 0 | identical | landed |
| 13 | 262,143 | 262,143 | 0 | identical | landed |
| 14 | **262,144** | 262,144 | 0 | identical | **landed** |
| 15 | **262,145** | 262,145 | 4 | not stored | **refused** |
| 16 | 262,145 | 222,145 | 4 | not stored | refused (multi-byte) |
| 17 | 262,144 | 222,144 | 0 | identical | landed (multi-byte) |
Rows 1 and 2 re-take the card's two endpoints as this run's OWN bracket
rather than inheriting them. Rows 3 to 15 bisect it.
**Interval reached: a single value.** 262,144 bytes stored; 262,145
bytes refused. The card forbade writing 262,144 as "known" unless
exactly that size landed and one byte more was refused — row 14 and row
15 are that pair. 262,144 is 256 KiB, which is why the guess was
tempting; it is reported here because the bisection landed on it, not
because it is round.
## The unit answer
Rows 16 and 17 fill the body with a 20,000-character run of U+4E2D
(three UTF-8 bytes each) plus ASCII padding, so byte length and
character length differ by 40,000.
- Row 16 sends **262,145 bytes but only 222,145 characters**. A cap
counted in characters — or in UTF-16 code units, the same number here —
would have taken it with 40,000 to spare. It was REFUSED.
- Row 17 is the positive control: the same multi-byte shape at **262,144
bytes / 222,144 characters** LANDED. So what refused row 16 is the one
extra byte, not the multi-byte content.
**The platform counts UTF-8 BYTES.**
Incidental, recorded because the tool's own header calls the cell
unmeasured: every landed write above read back `identical` — no
trailing-newline strip (these bodies carry none) and no synthesised
footer on an ISSUE body. The cell for a body that already ends in the
footer block stays unmeasured; none of these did.
The probe issue is closed `completed`, with the bisection table as its
final landed body.
## The constant, before and after
Before: `export const ISSUE_BODY_LIMIT = 65536;`, docblock "GitHub's
hard cap on an issue body ... A body that exceeds the cap is REJECTED by
the API".
After: `export const ISSUE_BODY_LIMIT = 262144;`, and the docblock now
carries the provenance the old one lacked — the UNIT (UTF-8 bytes), BOTH
sides of the bracket (`262,144 bytes STORED` / `262,145 bytes REFUSED`),
the DATE (2026-09-17) and the PROBE ISSUE (objectstack#18793) — plus the
correction that the refusal is silent rather than an API rejection, and
the reason the round number is not the reason.
## The guard and the budget, in one unit
The renderer's cut at `:19420` counted `line.length` against
`MARKDOWN_BODY_BUDGET`, and seeded its accumulator with `body.length +
indexText.length`. **That unit is UTF-16 code units** — JS `.length` —
while the platform refuses in bytes, over rows that are largely CJK
prose, where the two differ by 3x.
- The accumulator and the guard now count `bodyBytes()` (a new tiny
export, `Buffer.byteLength(..., 'utf8')`). The `+ 1` stays: the joining
newline is one byte in UTF-8.
- `familyLedgerReservation` is reserved out of the same budget, so it
returns bytes too — which is what its own header has said since it was
written ("in bytes"); it was measuring `.length`. Its two digit-slack
terms are ASCII digits, already byte-correct.
- H6 next door (`h6SeatBodyOversized`) has counted `Buffer.byteLength`
since it was written. The renderer now agrees with it and with the
platform.
- **`MARKDOWN_BODY_BUDGET` does NOT move — it stays 60,000, now BYTES.**
A cap that was mis-measured is a correction to a reading, not a licence
to print more; how much the anchor issue prints is a fold decision
nobody has taken, and this PR takes none. Because bytes are never fewer
than characters, the new guard cuts at or before where the old one did:
strictly narrowing, never widening, which is what the card asks for.
- The margin is stated rather than left to arithmetic, in the docblock
and in a pin: the budget sits **202,144 bytes below the measured cap**,
22.9% of it. The `FAMILY_LEDGER_WORST_CASE_BYTES` docblock's stale
sentence about "the 5,536-byte headroom ... (65,536)" is corrected in
the same place, with a note that its two measured figures were taken
with `.length` under a name that said BYTES.
## The pins
The pin at `:28367` is re-pointed to the measured cap and re-measured in
bytes, and twelve other size pins across the suite move to bytes with it
(they asserted `.length` against a budget the guard no longer counts
that way). A new floored battery is added in the shape PR #18756 landed
— `SELF_TEST_BATTERIES` gains `'ISSUE_BODY_LIMIT measured cap': 37` and
`SELF_TEST_BATTERY_FLOOR` rises 5 to 6, with the two sibling floor pins
that name the roster size updated. 40 cases register against it:
- **provenance** (7): the docblock attached to the constant names the
unit, both sides of the bracket, the date and the probe issue — anchored
on the docblock slice, not on the file text, where these assertion
strings would satisfy themselves; plus the two controls that the
extractor found a real docblock and not the whole file.
- **the cap** (4): the value, that it is 256 KiB exactly, that the
retired 65,536 is gone, and that the measured cap is four times it.
- **the unit** (4): `bodyBytes('U+4E2D')` is 3 where `.length` is 1;
ASCII agrees; an absent body is 0 and not a throw.
- **the guard** (7), with its firing controls inside: a 4,000-row CJK
report is trimmed to the BYTE budget and announces the trim; the row
text really is multi-byte (3x), so the bound is not vacuous; the block
it lays out fills the budget in bytes while its character count is under
half that; and the COUNTERFACTUAL, computed from that same run — how
many rows a character-counting guard would have laid out from the same
fixture, and what that body would have weighed.
- **the over-cap report** (6), the #4690 shape against the measured cap:
a fixture whose rows alone exceed 262,144 bytes untrimmed (asserted, so
it is not vacuous) renders under the cap AND under the budget, SAYS it
trimmed, points at the run log, and still never reaches the loud row.
- **the margin** (4) and **the floor** (6).
Read the counterfactual honestly, and the battery comment says so: it
overruns the renderer's BUDGET, not the platform's measured cap, which
no character count can reach at today's budget. The budget is the thing
that stops meaning anything when it is kept in the wrong unit — and the
thing that would become dangerous the day anybody raises it toward the
cap. That is an argument for landing the unit fix before, not instead
of, any budget decision.
## Ablation
From the COMMITTED fix (`5fc8e0ce57`), two legs, each mutating on disk,
proving the mutation by grep counts on both the removed and the injected
text AND by `git hash-object` against the HEAD blob, running the suite,
and restoring with `git checkout HEAD -- path` verified by hash equality
and an empty `git diff HEAD`, under a `trap ... EXIT INT TERM` with
absolute paths.
HEAD blob `153d10a0158ba27906a5400df8a9c486d77ae061`.
**Leg A — put `65536` back.** Mutated blob
`7d34ca5db0377d33d7e5062792dd54a51edbe763`; removed-text count 1 to 0,
injected-text count 0 to 1. Self-test exit 1, **5 cases red, every one
of them in the new battery**, nothing pre-existing red:
```
#18664 cap: the cap is the bisected value (got 65536, want 262144)
#18664 cap: ...which is 256 KiB exactly, checkable by hand (got false, want true)
#18664 cap: the retired 65,536 is gone (got true, want false)
#18664 cap: ...and the measured cap is four times it (got false, want true)
#18664 margin: ...by the stated margin, to the byte (got 5536, want 202144)
```
**Leg B — put the character-counting accumulator back** (`let used =
body.length + indexText.length + ledgerReservation;`). Mutated blob
`d1ec008854fb628f8822232cbfa4344ace4d2342`. Self-test exit 1, **9 cases
red**: two in the new battery (the CJK trim and the over-cap body's
budget bound) and **seven of the file's PRE-EXISTING budget pins** —
`markdown: ...and under the renderer's own budget`, `markdown: ...and
the body is still under budget`, `H17 budget`, the `④ budget` box,
`#13947 order`, `#13947 reserved` and `#13947`'s flood pin. That is the
intended direction and worth reading twice: those seven pins are red
under the old guard **because this PR re-measured them in bytes**. Under
`.length` they could not fail — and the reason they fail now is that
this file's own fixtures already carry enough multi-byte text (em dashes
in the row messages) that a character-bounded body overruns the byte
budget it was supposed to keep. The old guard was not bounding the thing
that matters, on this suite's own inputs.
Both legs restored: hash matches the HEAD blob and `git diff HEAD` is
empty. Working tree clean afterwards.
## Self-test
```
check-half-states self-test: 4921 cases pass. Batteries: H66 released queue card 183/172,
H19 judged-set founding 37/34, H65 tier declaration spelling 46/42,
H67 queued merged-delivery reading 152/142, H2/H47/H66 decorated ownership marker 98/94,
ISSUE_BODY_LIMIT measured cap 40/37.
```
4,881 cases before, 4,921 after.
## Derived gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree, no hand-fed path list —
38 commands derived against the change set it took from the merge base
itself (1 path: `scripts/pm/check-half-states.mjs`). Every one run, exit
code captured by redirect-then-`$?`, never through a pipe. Reconciled
with `--ran`: 38 derived, 38 run, 0 unrun.
```
0 node scripts/check-changeset-no-major.mjs --base origin/main
0 node scripts/check-changeset-no-major.mjs --self-test
0 node scripts/check-ci-filter-parity.mjs
0 node scripts/check-closing-keyword-parity.mjs
0 node scripts/check-closing-keyword-parity.mjs --self-test
0 node scripts/check-comment-mask-corpus.mjs
0 node scripts/check-declaration-mirrors.mjs
0 node scripts/check-declaration-mirrors.mjs --self-test
0 node scripts/check-scripts-symbol-anchors.mjs
0 node scripts/check-scripts-symbol-anchors.mjs --self-test
0 node scripts/check-self-test-wired.mjs
0 node scripts/check-self-test-wired.mjs --self-test
0 node scripts/check-self-test-workflow-commands.mjs
0 node scripts/check-self-test-workflow-commands.mjs --self-test
0 node scripts/check-whole-set-label-write.mjs
0 node scripts/check-whole-set-label-write.mjs --self-test
0 node scripts/pm/bare-root-worklist.mjs --self-test
0 node scripts/pm/board-snapshot.mjs --self-test
0 node scripts/pm/check-governed-queue-guard.mjs --self-test
0 node scripts/pm/sweep-closed-cards.mjs --self-test
0 pnpm check:agent-test-spelling
0 pnpm check:bash32-floor
0 pnpm check:changeset-gate-self-tests
0 pnpm check:cli-command-ids
0 pnpm check:closing-target-claim
0 pnpm check:commit-card-trailers
0 pnpm check:cross-package-test-inputs
0 pnpm check:driver-memory-census
0 pnpm check:entry-guard
0 pnpm check:nul-bytes
0 pnpm check:parse-guard
0 pnpm check:partof-closing-keyword
0 pnpm check:pm-half-states
0 pnpm check:pnpm-filter-targets
0 pnpm check:ratchet-remedy-authority
0 pnpm check:refd-timer-probe
0 pnpm check:watch-hint-literal
? pnpm check:pm-dispatch-gates -- see below
```
`pnpm check:pm-dispatch-gates` is the 38th. It runs past this
container's foreground ceiling, so it was started DETACHED with its
output redirected to a file, never under a foreground timeout. Its
verdict is reported in the delivery comment on #18664, read from that
log at report time; if it had not reached a verdict by then it is
recorded there as NOT MEASURED by name, not as a green.
Repo-wide `pnpm lint` (`eslint . --no-inline-config`): **exit 0**, whole
repo, no narrowing, run at `5fc8e0ce57`. A control-character scan over
the edited file (`grep -naP` over the C0 set plus DEL) matched nothing,
exit 1.
## Acceptance notes
Observations from this work, filed nowhere and changed nowhere, per the
scope rule:
- **Five workflow files carry the same unmeasured 65,536, about COMMENTS
rather than issue bodies** —
`.github/workflows/cross-repo-issue-closer.yml:166`,
`docs-drift-check.yml:557`, `merge-queue-triage.yml:178`,
`scheduled-full-run-card.yml:125` and `test-nightly-tiers.yml:436`, each
bounding a log tail or a report against "GitHub's 65536-character
comment limit". This PR measured the ISSUE BODY cap only; the COMMENT
cap is **not measured**, so nothing here shows those five are wrong —
what it shows is that they carry a number of the same provenance as the
one just falsified, in a unit ("character") that the body cap has now
been measured NOT to use. Worth a card and its own bisection; out of
this card's one-file surface. Dedupe words: `65536 comment limit
workflow log tail truncation`.
- **`FAMILY_LEDGER_WORST_CASE_BYTES`'s two measured figures (2,285 B /
3,754 B) were taken with `.length` under a name that says BYTES.** Their
pins are now measured in bytes and still fit the 6,000 ceiling, so
nothing is wrong; the numbers in the prose are simply readings in the
other unit. Noted in the docblock rather than re-measured, because
re-measuring them is a diff about that constant and not this one.
- **Raising `MARKDOWN_BODY_BUDGET` toward the measured cap is now
possible and is deliberately NOT done here.** It changes what the
standing patrol's anchor issue prints, which is a fold decision for a
seat or the maintainer, not a consequence of correcting a measurement.
Successor: none queued — it wants a card of its own if anybody wants the
longer report.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 034f5a3 commit 8904880
1 file changed
Lines changed: 227 additions & 31 deletions
0 commit comments