Uh oh!
There was an error while loading. Please reload this page.
fix(gate): let check:role-word exempt marked vendor-wire fences - #10787
Conversation
ADR-0090 D3's ratchet is per-file and EXACT, so no file anywhere has spare budget and the corpus cannot gain a single new occurrence of the reserved word. A route whose wire parameter is literally named `role` therefore could not have its request shape documented by anyone but the maintainer: `POST /api/v1/auth/organization/add-member` reads `body.role` with no snake_case alias, and #10050 had to describe a REQUIRED parameter without naming it. Per the maintainer ruling of 2026-08-21 (option B), the gate gains a narrowly-scoped exemption for upstream-owned vocabulary inside fenced code blocks MARKED as vendor wire payloads. Prose stays fully ratcheted — that is what D3 is aimed at, and D3's Word ban paragraph already names the better-auth boundary as its single documented exception, so this clarifies D3 rather than amending it. The ADR is untouched. The marking is what bounds the exemption, not the fence. A block is exempt only when the line directly above its opening fence is exactly `{/* os:vendor-wire <vendor> */}` (.mdx) or `<!-- os:vendor-wire <vendor> -->` (.md), naming a vendor declared in VENDOR_BOUNDARIES — the same per-extension comment convention `os:check` already uses. A marker that opts nothing in is a loud orphan rather than a silent no-op, an unclosed claimed fence exempts nothing, and every run publishes how many blocks it suppressed, so the exemption cannot widen unobserved. Today's corpus: 0 blocks, 0 occurrences. Self-test covers all three directions over a fixture proven to carry the word (marked fence passes, prose still fails, UNMARKED fence still fails), plus the positive control and every near-miss marker, driven both as predicates and through a real child process. Refs #10533 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32478655502 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Queue-build triage: environmental, not this PR. No fix will be pushed; one re-queue when I can.Following the triage checklist on the automated comment above. Item 2 — failing job unrelated to this PR — with the evidence. The failed job is Build Docs, and the automated extract could not read its step. I fetched the full log (209 lines). It fails in corepack, during install, before any test body runs: A TLS socket dropping while fetching the pnpm tarball from the npm registry. This PR changes exactly one file — Why a re-queue is the right response here, and not the reflex it usually isThe triage comment is right that blind re-queuing burns a full rebuild for every PR behind this one, and I am not doing it on a hunch. The standing rule this seat works to allows a re-run in exactly one shape: the job died before any test body executed — checkout, install, or runner loss. This is the install case, named explicitly, and the log shows no test ever started. The two harder branches do not apply:
Current state and what happens next
⛔ I cannot re-arm right now. GraphQL is exhausted (0/5000, reset 12:16:43Z) and both the draft flip and auto-merge go through it; REST core is fine at ~14.9k. This is a stated blocker, not a silent wait. After the reset I will re-check membership and re-arm once. If it fails the same way a second time, that is no longer environmental-by-assumption and I will treat it as real. ⛔ Nothing about this changes the PR's content: it implements the maintainer's option-B ruling of 2026-08-21T03:52:39Z, corpus delta is 0, and both ablation directions matched their predictions exactly (10/10 and 12/12). Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10533
The defect
scripts/check-role-word.mjsis a per-file, exact-count ratchet: a baselined file whosecount moves in either direction fails, and
--updateis ⛔ MAINTAINER-ONLY. Because it isper-file and exact, no file anywhere has spare budget — so the corpus cannot gain a single
new occurrence of the reserved word, and a route whose wire parameter is literally named
rolecould not have its request shape documented by anyone but the maintainer.
Measured, not hypothetical.
POST /api/v1/auth/organization/add-memberreadsbody.role(
readRole()inpackages/plugins/plugin-auth/src/organization-add-member.ts) and — unlikeuserId/organizationId/teamId— carries no snake_case alias, so the wire name isthat word and nothing else. #10050 had to describe a required parameter without naming it.
Reproduced on the base commit (
1793fb66fd), by adding the field to thehttpfence that pagealready has:
The ruling this implements
Maintainer ruling, 2026-08-21T03:52:39Z (issue comment
5364977770),verbatim and untranslated: 「其他接受」 — accepting option B:
scripts/role-word-baseline.jsonis untouched — option A's path is not taken here.ADR-0090 is not amended, and needed no change
D3's own Word ban paragraph already carves out this exact boundary in its own words:
So B clarifies what D3 always targeted — ObjectStack prose reaching for the word where
permission_set/position/business_unitis meant. The gate simply had no way to express"legitimate kind, new occurrence"; it could only express "frozen count".
docs/adr/**isuntouched.
What bounds the exemption: the MARKING, not the fence
Keying on "is a fenced block" would exempt every code block in the corpus — materially broader
than what was accepted. A block is exempt only when the line directly above its opening fence
is exactly the marker, naming a vendor declared in
VENDOR_BOUNDARIES(today:better-auth):.mdx{/* os:vendor-wire better-auth */}.md<!-- os:vendor-wire better-auth -->This is the repo's existing
os:checkconvention, per-extension for the same reason(
packages/spec/scripts/check-skill-examples.tsis the reference: MDX has no HTML comments,and the MDX form renders as literal text in
.md). The marker is explicit, greppable(
git grep os:vendor-wire) and impossible to apply by accident — it opts nothing in unlessall three of syntax-for-this-extension, the exact token, and a declared vendor hold.
Four properties keep it from widening on its own:
marker reads as intentional while its block is still fully counted.
(CommonMark), so honouring one would hand a single line the whole rest of the file.
docs page. D3 documents exactly one boundary.
rediscovered:
Deliberately NOT extended to inline code spans. A backticked word in a sentence is prose with
backticks around it; inline code is everywhere and exempting it would leave the ratchet nothing.
A prose mention still costs a baselined occurrence — including the route path
/organization/update-member-rolewritten into a sentence, which the card names as asecond-order bite. The remedy there is the same one every author has: put the wire shape in a
marked fence.
Corpus delta: 0
0blocks exempted,0occurrences suppressed. Nothing incontent/docsorskillsistouched by this PR, so no existing count moves: the scan reads the same 225 files and the
same 43 baselined files / 129 occurrences before and after. The only difference in the
gate's output on a clean tree is the new
Exempt:line.Evidence
Gate union re-run at final HEAD
93d559b5ac, exit codes captured before any pipe.node scripts/check-role-word.mjs --self-testEXIT=0node scripts/check-role-word.mjsEXIT=0—OK, no new occurrencesnode scripts/check-cross-package-test-inputs.mjsEXIT=0—OK: 13 package(s) read outside themselves, all declarednode scripts/check-parse-guard.mjsEXIT=0—✓ check:parse-guard: 125 scripts/ file(s)node scripts/check-nul-bytes.mjsEXIT=0—no raw ASCII control bytesnode scripts/pm/dispatch-gates.mjsEXIT=0— derived from the real changesetCI runs
check:role-wordas--self-test && <gate>(rootpackage.json), so the new self-testis reachable in CI rather than a phantom check.
Before / after, on the real docs file
Same fenced addition to
content/docs/permissions/authentication.mdx, all three directions(each reverted afterwards — this PR changes no docs):
1793fb66fdhttpfence, markedEXIT=1count grew 4 → 5EXIT=0,Exempt: 1 … suppressed 1EXIT=1EXIT=1count grew 4 → 5EXIT=1EXIT=1count grew 4 → 5, not 6 — the marked block in the same file stayed exemptAblation — both directions, predicted vs observed
An exemption can rot two ways, so it was mutated both ways. Each mutation was confirmed on disk
by anchor count (injected token present, replaced text absent), not by an editor's exit code;
each was restored with
git checkoutfrom the commit and the tree verified byte-identical(
git status --porcelainempty, ablation markers0).vendorWireClaim→null)Composition matched the prediction exactly in both runs. The first fells (B1), (B2) and both
(B1, program)legs; (B3) stays green there on purpose — it asserts the absence ofexemption, so removing the exemption cannot break it. That is exactly why the second ablation
exists, and it is led by:
plus the five near-miss markers, three orphan legs, the cross-spelling leg, the
marker-inside-a-fence leg, and
(B3, program).Self-test shape (the ruling's required set)
All three directions over a fixture proven to carry the word — that is the positive control,
and it comes first because every other leg is vacuous without it (a fixture that had lost the
word would make (B1) pass by saying nothing at all):
exempt — the exemption suppresses a block, never a file.
Then, for each way the marking could stop bounding it: a blank line between marker and fence,
the other format's spelling, an undeclared vendor, the bare token, a marker above prose — each
opts nothing in and is reported as an orphan. Plus backtick run-length (a ```` fence wrapping