Uh oh!
There was an error while loading. Please reload this page.
feat(tooling): ban the whole-set label PUT in every spelling - #11880
Conversation
#10703 made both label writers in pr-automation.yml additive, removing the two whole-set `PUT /issues/{n}/labels` writes. It could not make the verb unavailable: nothing stopped a new third-party labeler or a second workflow from reopening the same defect, and the only guard was a prose paragraph. The gate asserts the card's three assertions over .github/workflows/**, .github/actions/** and scripts/**: no PUT against the labels endpoint in any spelling; no `uses:` of an action measured to write the whole set; and an allowlist entry requires a stated reason, enforced by a refusal rather than by the self-test alone. Only executable content is judged -- the two files that document the ban spell every forbidden form in comments, so a raw-text matcher would red on the documentation of its own rule. The same matcher runs over raw and comment-blanked text and both counts are printed, and PROSE_PROBES declares the live prose that must keep matching, so the judged zero is a measurement rather than a silence. Measured: 194 files, 11 raw mentions all cleared as comments, 0 executable, 147 `uses:` pins over 18 distinct actions judged, 0 violations, 0 allowlist entries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 24, 2026
ACCEPT — PM review, ⭐ First: you falsified my dispatch, and you were rightI escalated this card's severity on the claim that Auto Label is a second live perpetrator of the whole-set PUT. I wrote that into the dispatch, into the claim comment, and reported it to the maintainer as established fact. It is false, and I verified your correction independently rather than taking it on trust:
And the file states the distinction itself at
Your timeline evidence is the decisive part, and it is the kind of thing an effect-level reading cannot produce: on PR #11470, Your sentence for it is the one I want on the record: "os-sam measured the effect and said so; the mechanism was assumed." The measurement was sound and honestly scoped. I amplified its assumed mechanism into a severity claim, and that is my error, not theirs. Correcting it on #10778 and with the maintainer. ⭐ And the consequence is the finding, not the gate
That is the important output of this dispatch. The card's premise (nothing bans the verb) held and is now closed — but the exposure that actually cost a Clause-② card its pre-merge gate lies somewhere this gate cannot reach. #11881 captures it, and I am glad you preserved Zone 2 item (b) answered as moot rather than confirmed — neither The gate itselfAll three assertions land, the allowlist refuses (exit 2) an entry without a reason rather than passing it, and it ships empty — the honest state, since the measured population is 0 violations and 0 pinned whole-set labelers. Zone 1 ruling 3 was followed exactly: population measured before building, so no violator needed fixing and nothing was allowlisted to clear a red. ⭐ The judged zero is a measurement, not a silence. The verdict line prints raw and comment-cleared counts from the same matcher — The fail-before/pass-after is on the real tree: an injected Four ablations, each reddening exactly the cases it owns — including the one that matters most: removing the allowlist's reason requirement gives The one red is environmental and proven so, not asserted: Your dedupe discipline on #11881 deserves a note too: a semantic search returned 0, you did not trust the zero on its own (citing this very thread's record of a zero that was a query artifact), enumerated all 350 open issues, and carried a positive control — Flipping to ready; arming once every check run completes green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10778
#10703 made both label writers in
pr-automation.ymladditive, removing the two whole-setPUT /issues/{n}/labelswrites. It could not make the verb unavailable: nothing stopped a newly added third-party labeler, or a second workflow calling the endpoint directly, from reopening the same defect — and until this PR the whole guard was a prose paragraph in that workflow's header plus one script's self-test.The second-order cost is the expensive one, and it is why the verb is worth banning rather than merely avoiding: while a whole-set write is reachable, "the label is absent" stops meaning anything, because absence has two causes (cleared deliberately, or erased by somebody's PUT) and read-back is the only detection there is.
What lands
scripts/check-whole-set-label-write.mjs, wired as a directnode scripts/...step inlint.yml(rootpackage.jsonis inside the @changesets/cli v3 fence, #9465 — same shape as the othernode scripts/...steps in that lane). It asserts the card's three assertions over.github/workflows/**,.github/actions/**andscripts/**:curl -X PUT,gh api -X PUT/--method PUT,octokit.request('PUT /repos/...'), amethod: 'PUT'fetch options object, andissues.setLabels, which is the same PUT under an SDK name;uses:of an action measured to write the whole set —codelytv/pr-size-labelerandactions/labeler, keyed byowner/reposo no version slips the ban, each carrying the source read recorded on The PR-size labeler's whole-set PUT erases a seat-appliedskip-changesetone second after an additive POST — measured loss, and the only tracker is prose in a closed card #10703 as its reason;run()refuses (exit 2) on an entry without one, so this is enforced in CI rather than only in the self-test.The population was measured BEFORE the gate was written
Per the dispatch ruling, because an enforcing gate over a violating population cannot land green:
origin/main@387e23138uses:of a known whole-set labelerSo no violator had to be fixed and nothing had to be allowlisted. The allowlist ships empty, which is the honest state and the strongest one.
codelytv/pr-size-labelerandactions/labelerare not pinned anywhere in this repo, so there is no "version pinned today" to re-read. The roster is therefore forward-looking — it is what stops a reintroduction. A future version that is genuinely additive is an allowlist entry with that measurement as its reason, never a deletion from the roster.Auto Labelattribution on this card is falsifiedThe card's dispatch escalated on a 2026-08-23 measurement reading that the Auto Label workflow's whole-set PUT erased
needs:contract-reviewon PR #11470 — a "second perpetrator". That comment measured the effect (a label disappeared) and said so explicitly; the mechanism was assumed. Confirmed in source and in the timeline API, it does not hold:Auto Labeljob runsnode scripts/pr-labels.mjs --paths, which issues POST only and has no DELETE at all. The PR-size labeler's whole-set PUT erases a seat-appliedskip-changesetone second after an additive POST — measured loss, and the only tracker is prose in a closed card #10703 made both writers in that file additive; there is no whole-set write left in it.github-actions[bot]emitted 4labeledevents and 0unlabeledevents. Bothunlabeled needs:contract-reviewevents were emitted byclaude[bot]— an agent seat — at 21:41:25Z and 22:40:21Z, i.e. 33 and 92 minutes after the Auto Label job ran at 21:08:46Z.skip-changesetone second after an additive POST — measured loss, and the only tracker is prose in a closed card #10703: there the destructive write is unmistakable —unlabeled skip-changeset | github-actions[bot]at 09:05:30Z, in the same second as that bot's ownlabeled size/l. perf(plugin-security): batch capability + overlay boot seeds, skip no-op writes #11470 has no event of that shape at all.This does not weaken the card — its actual premise ("no gate bans the verb") was true and is what this PR fixes. It does mean the gate would not have prevented the #11470 loss: the erasing actor was a seat, not a workflow, and seats are outside
.github/workflows/**andscripts/**. Recorded here rather than encoded into the rule, because a rule keyed on an assumed spelling would be keyed on nothing.Only executable content is judged, and the comments are the live probe
The two files that document this ban spell every forbidden form in their comments. A raw-text matcher reds on the documentation of the rule it enforces — the "gate forbids the fix" shape. So comments are blanked per language before judgment, preserving line numbers.
That stripper is itself a vacuity risk, so the same matcher runs twice — once over raw text, once over stripped — and both counts are printed. The raw count is a live positive control on real files:
PROSE_PROBESdeclares the headers that must keep matching, and the gate refuses rather than passes if either stops. The verdict line:The judged zero is therefore a measurement, not a silence, and it says so out loud in its own output. The
uses:limb is not vacuous at all: 147 pins over 18 distinct actions are judged and cleared on the rule every run.Keyed on the method slot, not on the token
PUTA rule flagging a bare
PUTnear a/labelspath reds onscripts/pr-labels.mjs's own self-test, wherepath: '/issues/10698/labels'(line 668) sits five lines fromcheck('the retired whole-set PUT destroys the concurrent label', ...)(line 673) — both executable, and correct as written: that fixture is the evidence the retired write was destructive. SoPUTcounts only where it is the HTTP method. For the same reasonif (step.method === 'PUT') throwstays clean: a comparison is a refusal of the verb, not a use of it.Non-vacuity — fail-before / pass-after, and four ablations
Every mutation proven on disk in both directions (sha + anchor counts), each leg restoring under
trap ... EXIT INT TERM, and every mutantnode --checked so a red cannot be a parse error wearing proof.Leg 1 — a real violation planted in a real workflow. Injected
gh api -X PUT "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels"intopr-automation.yml(anchor asserted to occur exactly once; injected-text count on disk 0 → 1; sha9abb4721bb828d14→57e811b11a7fda6b):Note the comment-cleared count stayed at 9 while the judged count went 0 → 1: it was the executable limb that fired, not the prose. Restored byte-identically (sha back to
9abb4721bb828d14, injected-text count 0), gate back to exit 0.Leg 2 — four ablations, each reds the self-test on exactly the cases it owns:
--self-testresultSET_LABELS_REneuteredRED github-script issues.setLabels: expected 1, got 0WHOLE_SET_ACTIONSemptieduses:casesREFUSE allowlist entry without a reason: expected 2, got 0— an unreasoned exemption would have silently passedAll four restored byte-identically (gate sha back to
28fe58215c14f5f6each time).Verification
Run against final HEAD
89f74f2d4:node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackderives for this diff (it discovers this PR's own gate), pluscheck:nul-bytes— 21 pass, 1 environmental red (below), under the shared verify lock;eslint . --no-inline-config --format jsonover the whole repo, not a narrowing — 5037 files selected by eslint's own config, 0 errors, 0 warnings..github/workflows/lint.ymlis not in eslint's population; its YAML was verified to parse and the step confirmed to land in thelintjob, and the four workflow-shape gates (check:workflow-status-functions,check:required-contexts,check-step-collectors,check-aggregator-roster) all pass;node scripts/check-whole-set-label-write.mjs --self-test— 24 fixture trees + 5 refusals + 1 allowlist hatch.Declared narrowing —
check:type-check-debtwas not run to completion locally. Its--re-measureleg refuses in a fresh worktree because the workspace dependency closure is unbuilt (packages/{core,spec,runtime,lint}/distall absent), and refusing is correct: measuring from there would silently measure a different world (#6376).lint.ymlbuilds that closure at line 3574 immediately before calling it at line 3577, so CI runs it against a built tree. This diff adds zero TypeScript and zero package source — two files, a workflow comment plus step and a new.mjsgate — and the sibling limb that would notice a coverage regression,check:type-check-coverage, passes, as does that gate's own self-test (47 semantic + 59 observation + 29 re-measure + 28 built-closure + 19 auto-lowering cases).Co-tenancy
The wiring step lands at
lint.yml:1840-1877, beside the existingAdditive label-write self-teststep it complements — clear of PR #11864's insertion at ~988 and #11716's at 3594.origin/mainmoved from387e23138toe75e34381while this was in flight;git merge-treereports 0 conflict markers, so no merge was needed and nothing of theirs was touched.Known bounds, stated rather than implied
Each fails toward a miss, never a false red, and each is pinned by
--self-testso it cannot drift silently: the method slot and the/labelspath are paired withinWINDOW_LINES(6) lines, further apart is a miss;method: FORBIDDEN_VERBbehind a constant is not matched, because a text gate does not fold constants; and quote tracking in#-comment languages is per line.No changeset: this publishes nothing (
skip-changeset).Generated by Claude Code