Skip to content

fix(core): remove ResolvedAuthzContext.authRefusal, a published member nothing ever read - #16442

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-14273-remove-dead-authrefusal-field
Sep 7, 2026
Merged

fix(core): remove ResolvedAuthzContext.authRefusal, a published member nothing ever read#16442
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-14273-remove-dead-authrefusal-field

Conversation

@claude

@claude claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #14273

Developer round for the domain:engine lane, session session_01ARYe3yQTQCUFm5qPYNgKaJ, dispatched by the PM seat in card comment 5563509663 on the A1 ruling (5522741447; A in 5507414114; cross-reference 5537709877). Contract tier: this diff is a published-type narrowing and its claim comment declares Clause-②: yes.

The act, as ruled

ResolvedAuthzContext.authRefusal is removed — the declaration and both writers in packages/core/src/security/resolve-authz-context.ts — under ADR-0049 enforce-or-remove. The ruling's sentence this PR is checked against: "the refusals themselves keep firing and keep failing closed exactly as today." They do. ⛔ No transport change: both wires keep answering the generic anonymous 401 UNAUTHENTICATED; no status code, body or header moves; a holder of someone else's key learns nothing. ⛔ The two warnApiKeyRefusal lines (#15256 / 2A, the ruled operator exit, already on main at the admission and post-grant decision points) and the session mirror are untouched — they belong to that card's ruling and are what makes this removal cost nothing.

What changed, file by file

  • packages/core/src/security/resolve-authz-context.ts — the member and its docblock are gone (−20 lines); the admission writer (−1) and the post-grant writer with its inline organization_membership_ended message text (−6) are gone. The warnApiKeyRefusal docblock's cross-reference ("[finding] ResolvedAuthzContext.authRefusal has zero transport readers — both #8287 refusal reasons collapse to the generic anonymous 401 on every wire #14273's A1 ruling REMOVES the field in its own PR — not removed here") would read false the moment this lands, so it now states the history in the past tense and names the line as the one place the refusal REASON is observable.
  • packages/core/src/security/authz-store-unavailable.ts — the rationale block A1 named (why AuthzStoreUnavailableError is a throw and not a field). It is a recorded rejection on a MEASUREMENT and is kept, rewritten so it cites the measurement that was acted on ("from [finding] API keys carry no organization — under the isolated posture a minted key reads no org data at all (no leak, but the key surface is inert) #8287 until [finding] ResolvedAuthzContext.authRefusal has zero transport readers — both #8287 refusal reasons collapse to the generic anonymous 401 on every wire #14273 removed it, authRefusal had zero consumers … a reading [finding] ResolvedAuthzContext.authRefusal has zero transport readers — both #8287 refusal reasons collapse to the generic anonymous 401 on every wire #14273 acted on by deleting the field") instead of a shape that no longer exists. Same line count.
  • packages/core/src/security/resolve-authz-context.test.ts — the 8 authRefusal assertions are relocated, not retired: two file-level helpers read the console.warn line, and every ctx.authRefusal?.reason pin becomes apiKeyRefusalReasons(warnSpy) equal to exactly ['organization_membership_ended'] or ['organization_required'], every authRefusal undefined pin becomes zero refusal lines. The distinction between the two refusal reasons is therefore still pinned — now on the surface the operator reads. Plus two new pins per the ruling ("one pin that the field is gone"): on both refusal paths the envelope has no authRefusal own-property.
  • packages/rest/src/execctx-authz-input-seam-reachability.test.ts — A1 named it. The §2 mechanism positive control now reads the warn line (API key refused (organization_membership_ended), exactly one), and the measured-permissive leg pins zero refusal lines; the wire assertions beside them are unchanged.
  • packages/plugins/plugin-auth/src/membership-ended-session.ts — a docblock cited authRefusal.reason: 'organization_membership_ended'; it now cites the ApiKeyRefusalReason value the resolver still mints for the warn line. Comment only, same line count.
  • packages/plugins/plugin-auth/src/session-positions-security-axis.test.ts — two comments anchor resolve-authz-context.ts:815; the removals above that line move it to :788, so the anchors are repointed. Comment only.
  • .changeset/membership-ended-session-revoke.md — a pending changeset's parenthetical named authRefusal.reason; one token, now ApiKeyRefusalReason, so the release note stays true in the release that ships both.
  • .changeset/core-authrefusal-field-removed.md — new, see Carriers.

Not touched, by fence: packages/core/package.json, packages/core/examples/**, packages/rest/src/rest-server.ts, rest-server-meta-read-org-scope.test.ts, api-key.ts (ApiKeyRefusalReason and ApiKeyAdmission stay — A1 says so, and ApiKeyAdmission.refused.message is a member of a different published type this card does not rule on).

Clause ② — measured, not recalled

@objectstack/core publishes files: ["dist", "README.md", "CHANGELOG.md"], root exports["."].types = ./dist/index.d.ts (and ./logger). Instrument: both legs built with tsup per package (no turbo on the path) under the shared verify lock, every declaration file under dist/ diffed, hunks classified.

reading base ac76425f0 head 5ecf881fc
dist/index.d.ts sha256 (16) 5803ef6e6fcfd22a ccfe43db0cf6a985
dist/index.d.cts sha256 (16) 12896688eacb1e44 f9b4133c5a920903
dist/logger.d.ts / .d.cts ae61dbc638be17f5 ae61dbc638be17f5 (identical)
dist/index.d.ts mtime 1788743041 1788743504 (rebuild proven)
exported name set, index.d.ts 242 242 — ADDED none, REMOVED none
interface ResolvedAuthzContext member lines (comments stripped) 17 13
control: interface ResolveAuthzInput block identical both legs

Hunks in index.d.ts (and byte-for-byte the same two in index.d.cts): (1) @@ -2094,13 +2094,13 @@ — JSDoc only, the authz-store-unavailable.ts rationale prose; not surface. (2) @@ -2271,29 +2271,6 @@ — the declaration text authRefusal?: { reason: ApiKeyRefusalReason; message: string; }; and its docblock removed from ResolvedAuthzContext; surface, a narrowing. The exported name set is blind to it by construction (a member of an already-exported type), which is why the member-level reading is the one that answers. Sourcemaps differ (they embed sourcesContent; comment edits show there) and are not a contract face. packages/spec/api-surface/** is not an instrument here (core is not spec).

Clause-②: yes — limb 1, a published declaration narrowed. Limb 2 (what a door accepts or refuses) does not move: the refusals fire identically on both legs (the relocated pins are green on both, see the reverse check), and no wire answer changes.

Sibling pin: objectui at .objectui-sha a472b0716: authRefusal 0 files; controls resolveAuthzContext 6 files, @objectstack/core 14 files.

Reverse verification (committed tree, absolute-path trap, blob-proven)

Mutation leg: git restore --source=ac76425f0 -- packages/core/src/security/resolve-authz-context.ts (writes the tree only) re-added the member and both writers on top of the committed head. Proven on disk before measuring: whole-line anchors authRefusal?: { reason: ApiKeyRefusalReason; message: string }; 0 → 1 and ctx.authRefusal = { reason: admission.reason, message: admission.message }; 0 → 1; blob e4f015df7 (HEAD) → 9928f2cfc (== the BASE blob). Predicted direction: exactly the two [#14273] own-property pins red, everything else green — because the warn line the relocated pins read is independent of the field.

Observed: Tests 2 failed | 86 passed (88) — the two failures are [#14273] a post-grant refusal answers an envelope with no authRefusal and [#14273] an admission refusal answers an envelope with no authRefusal either; all six relocated reason pins stayed green with the field present (VERDICT command-exit 1, the expected red). Restore leg: git checkout HEAD -- ABSOLUTE_PATH from an EXIT INT TERM trap; proven by git hash-object == HEAD blob e4f015df7, git diff HEAD --stat empty, git status --porcelain empty, field anchor back to 0. No rebuild was needed for this leg: the core suite imports the resolver from src; the rest leg reads dist/ and was not re-driven under the mutation (declared).

The PM's Zone-2 assumptions, re-measured on this tree

  • 2.1 zero runtime readers — held. git grep authRefusal over all tracked files at base: 18 lines in 6 files; firing control resolveAuthzContext = 139 files under packages/. Writers 2, declaration 1, runtime readers 0; the rest is prose and test assertions. Structural readers (destructuring, index access, spread, Pick/Omit in another package) would still have to spell the token — and none of the 139 consumer files does; the objectui sibling has 0 with firing controls.
  • 2.2 three prose-only references — falsified in count (there are four, plus the field's own docblock). The PM's table missed .changeset/membership-ended-session-revoke.md:21. Per site: resolve-authz-context.ts:184 rewritten (would read false); authz-store-unavailable.ts:49-50 kept as load-bearing history, re-cited to the measurement acted on (A1's own instruction); membership-ended-session.ts:97 re-cited to ApiKeyRefusalReason; the changeset parenthetical re-cited the same way.
  • 2.3 nine test assertions — falsified in count: ten (resolve-authz-context.test.ts ×8 — the PM's ×7 missed the #15409 session-arm pin at old :1666 — plus rest ×2). None skipped, disabled or quarantined; all relocated onto the warn line; nothing genuinely retired, because every one of them pinned behaviour (which reason, or that no refusal fired) and not only the field.
  • 2.4 narrowing visible in the published .d.ts — held, with the caveat measured above: visible at member level only; the name set reads 242→242.
  • 2.5 nothing outside packages/core and its tests changes — falsified, narrowly. Two comment-only citations in packages/plugins/plugin-auth and one token in a pending changeset were made false by the removal and are repaired (no published artefact of plugin-auth moves — comments and a test file). No consumer code changes: typecheck of the direct consumers whose closures were built locally — @objectstack/rest, @objectstack/plugin-sharing, @objectstack/service-datasource — exit 0 against the rebuilt core declarations; the workspace-wide compile is CI's TypeScript Type Check and was not re-run here.

Carriers

  • Changeset .changeset/core-authrefusal-field-removed.md: @objectstack/core minor — the act is a breaking narrowing, and scripts/check-changeset-no-major.mjs:55-66 says the level is not the carrier during the launch window; the carriers are the **BREAKING** banner in the body and the ADR-0087 disposition, both present. Disposition: not-required (runtime-interface-only packages/core/src/security/resolve-authz-context.ts#ResolvedAuthzContext) — the ADR-0087's changeset disposition has no category for published runtime TS interfaces with no metadata surface — the #8277 exemption argument is correct, unverifiable, and will be re-litigated #8299 category built for exactly this shape (a published runtime interface lost a member; no schema, spec declaration or stored representation; the compiler is the channel). The gate verified it: check-adr-0087-registration --base ac76425f0 → exit 0, "verified: … #ResolvedAuthzContext (interface)". check-changeset-no-major exit 0; check-empty-changeset exit 0. Modelled on d8024f0:.changeset/core-plugin-type-closed-set.md; that precedent's no-migration-prescription category predates runtime-interface-only, whose predicate 4 is the positive, re-runnable reading this body prefers.
  • needs:contract-review hung on the card and on this PR, additively, the moment this PR exists (5536347794 removed the premature one).

Verification, all at 5ecf881fc (clean tree)

  • pnpm --filter @objectstack/core exec vitest run --maxWorkers=2 src/security/resolve-authz-context.test.ts src/security/authz-store-unavailable.test.ts src/security/api-key.test.tsTest Files 3 passed, Tests 145 passed; lock VERDICT command-exit 0.
  • pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/execctx-authz-input-seam-reachability.test.ts1 file, 27 passed, VERDICT command-exit 0 (imports @objectstack/core through its rebuilt dist/, so this leg drove the published artefact).
  • pnpm --filter @objectstack/core typecheck exit 0 (src + examples + check:test-typecheck: 4 files / 4 errors / 4 pinned, unchanged ledger — the edited test file is not in it, so it is compiled clean). @objectstack/rest, @objectstack/plugin-sharing, @objectstack/service-datasource typecheck exit 0.
  • eslint, targeted: 6 touched .ts files, --format json → 6 files, 0 errors, 0 warnings; all 6 inside the config's population (--print-config resolves); eslint.config.mjs has no parserOptions.project (no type-aware rules), so this diff cannot move any untouched file's verdict — a measured narrowing of pnpm lint, whose full sweep is CI's.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (8 paths vs merge base ac76425f0): 25 node-script gates exit 0 (including check-system-context-census, check-tenant-audit-census, docs-audit/*, check-closing-keyword-parity); 24 pnpm check:* families exit 0 (authz-resolver, cross-package-test-inputs, dts-closure — 25 built packages swept, 94/94, core in the set — engine-double-contract, nul-bytes, published-files, test-source-alias, type-check-coverage, type-source-resolution, …). Not measured locally: check:dual-build-cjs-loads and check:type-check-debt both exit 3 (their own "PREREQUISITE NOT MET — nothing was measured": they need the whole workspace built); CI's full-build job carries them.

Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ


Generated by Claude Code

…ber with zero readers

Maintainer ruling A (2026-09-02, ADR-0049 enforce-or-remove), re-affirmed as A1
(2026-09-03) with the carriers a published-type narrowing owes. The two
posture-conditional API-key refusals keep firing and keep failing closed; both
transports keep answering the generic anonymous 401. The refusal reason stays
observable on the server-side `warnApiKeyRefusal` line (#15256 / 2A), where the
relocated pins now read it. Prose that named the field is repaired, not erased:
the rationale block in `authz-store-unavailable.ts` now cites the measurement
that was acted on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions github-actions Bot added the size/m label Sep 7, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/plugin-auth, touching 4 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/core/src/security/authz-store-unavailable.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx (via resolveAuthzContext (symbol, a top-level function))
  • content/docs/permissions/authorization.mdx (via resolveAuthzContext (symbol, a top-level function))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via resolveAuthzContext (symbol, a top-level function))
  • content/docs/releases/v17.mdx (via resolveAuthzContext (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/core/src/security/authz-store-unavailable.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 30 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c14ebfeb29d8d3bc0ae9b148132a78b86dec94bcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 0809e0b90c677c6155ee89b479a08f6a4fdc5112 — the merge of head 5ecf881fc6ac9b1a31fec37fda75b81058f21fc0 into base c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0809e0b90c677c6155ee89b479a08f6a4fdc5112 && git checkout 0809e0b90c677c6155ee89b479a08f6a4fdc5112
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc 5ecf881fc6ac9b1a31fec37fda75b81058f21fc0 && git checkout -B drift-repro c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc && git merge --no-ff 5ecf881fc6ac9b1a31fec37fda75b81058f21fc0

node scripts/docs-audit/affected-docs.mjs --json c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c14ebfeb29d8d3bc0ae9b148132a78b86dec94bc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Contract review PASSED. Release authorised at 5ecf881fc6ac⚠️ the un-draft is blocked on a rate limit, not on this PR.

domain:engine dispatching seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ, 2026-09-07T02:1xZ. ⛔ This seat submits no approving review on a governed-surface PR; the review below was done by a separate contract-tier seat and this comment is the release record, not an approval.

The three conditions, each re-measured here rather than relayed

# condition reading
contract-review PASS bound to the head that will land PASS at 5ecf881fc6ac9b1a31fec37fda75b81058f21fc0; head re-read after the verdict and unmoved
check-clause2-carriers --pair 16442 REAL_EXIT=0 (redirect-then-read) — "the clause-② declaration is readable in the fixed spelling and both carriers agree". Checker blob 751b4a6e4 == origin/main's
completed == total AND failing == 0 37/37, 0 failing, mergeable_state=clean — ⛔ not a required-checks subset

needs:contract-review cleared from both carriers, comparative read-back proving before − label exactly: PR [documentation, size/m, tests, tooling], card #14273 [security, pm:dispatched, domain:engine, priority:p3].

⭐ What the review established, because a PASS is only worth its measurements

  • Limb 1 fires, at member level only. Both legs built with tsup directly (no turbo), rebuild proven by mtimes and by a byte-identical head rebuild across all 6 artefacts. dist/index.d.ts and dist/index.d.cts each lose exactly authRefusal?: { reason: ApiKeyRefusalReason; message: string; } from interface ResolvedAuthzContext (members 15 → 11). ⭐ The exported name set is 260 → 260 — blind to this by construction, which is precisely why it is never the instrument. Consumer compile probe: c.authRefusal is TS2339 against the head .d.ts and compiles against the base .d.ts, with a control member (c.posture) compiling on both.
  • Limb 2 does not fire — and this is the limb a .d.ts diff cannot see, so it was answered by running the door: resolveAuthzContext driven from the built dist/index.js on both legs across six scenarios. Warn-line reasons, userId, tenantId and every other envelope key identical; the only delta is the authRefusal own-property. The emitted runtime diff is exactly the two writer removals and nothing else.
  • The ablation leg the round declared undone was run. The rest cross-package suite against the base-built core dist is 27/27 — so the relocated pins read a signal the field's removal does not touch, which is exactly why the two own-property pins in core are what go red under the reverse mutation (2 failed | 86 passed, reproduced, mutation and restore blob-proven).
  • The ADR-0087 category was checked, not assumed. runtime-interface-only is right: ADR-0087 D7 (docs/adr/0087…:603) defines it for "a published runtime TS interface … compiler-carried", and gate ADR-0087's changeset disposition has no category for published runtime TS interfaces with no metadata surface — the #8277 exemption argument is correct, unverifiable, and will be re-litigated #8299's predicates 1–4 were verified positively, not merely by exit 0. The precedent this PR was measured against (d8024f0, no-migration-prescription) predates ADR-0087's changeset disposition has no category for published runtime TS interfaces with no metadata surface — the #8277 exemption argument is correct, unverifiable, and will be re-litigated #8299, which the gate's own docblock records as the older catch-all held by a detector miss. ⇒ choosing the narrower category is correct, not a deviation.
  • Governing-text divergence, stated rather than smoothed over. AGENTS.md:1021-1022 asks for an arrow-form FROM → TO mapping; for this category the gate at check-adr-0087-registration.mjs:3378-3390 would refuse exactly that form. The changeset states the fix in prose instead. Where the two differ the gate docblock is the operative policy — the same lesson that cost feat(platform-objects): sys_job.timezone and sys_report_schedule.timezone are validated against the IANA domain #16296 a remediation PR.

⚠️ One correction this comment owes, and one declared blind spot

Correction to the PR body. It says "no published artefact of plugin-auth moves — comments and a test file". Measured by ablation, that is imprecise: @objectstack/plugin-auth (published, files: ["dist"]) does move — dist/index.d.ts and dist/index.d.mts differ by one JSDoc-only hunk (0 non-comment lines; the edited docblock belongs to the exported MEMBERSHIP_ENDED_REVOKE_REASON), plus two sourcemaps. It is not a surface change and owes no changeset — but "no published artefact moves" and "no published surface moves" are different claims, and only the second is true. Recorded here rather than by editing the body, because a PATCH to a PR body drops the server-appended footer.

Declared blind spot, now with a reason instead of a shrug. The review could not measure an out-of-repo cloud consumer of ctx.authRefusal. This seat checked why: the account's reachable repository set is objectstack, objectui, objectos — a listing whose control fires (an unfiltered list returns those three; a cloud filter returns zero). ⇒ there is no cloud repository reachable from here; the gap is unmeasurable rather than unmeasured. The sibling contract this repo actually states is .objectui-sha, and objectui was measured at pin a472b0716: 0 files naming authRefusal, against firing controls of 6 (resolveAuthzContext) and 14 (@objectstack/core).

⚠️ Why it is still a draft

update_pull_request draft:false is refused with "API rate limit already exceeded for user ID 19182527" — a user-scoped budget, disjoint from REST's (core reads ~14600/15000 at this moment). ⛔ REST PATCH is not a substitute: re-measured on the sibling PR minutes ago, it returns 200 and changes nothing. Nothing about this PR is holding it; the un-draft and auto-merge are re-attempted on the next check-in.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 7, 2026 04:57
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 34086147182 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (4/6) — 失败步骤: Run this shard's tests

    @objectstack/plugin-email:test:  FAIL  src/email-service.queue-delivery.test.ts > EmailService — queue delivery on > still refuses the queue for attachments OVER the limit, and stores nothing (#5177)
      ↳ 失败原因: @objectstack/plugin-email:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 2 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Dequeued at 05:42:22Z on a queue failure that is not this PR's — measured. Re-queue is owed and is itself blocked on a rate limit.

domain:engine dispatching seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ, 2026-09-07T05:4xZ.

What failed

Queue CI run 34086147182 on the queue branch queue/main/pr-16442-… (head 6c28f29e) — 17 jobs, exactly one failing shard, Test Core (4/6):

FAIL  packages/plugins/plugin-email/src/email-service.queue-delivery.test.ts:279
  > EmailService — queue delivery on
  > still refuses the queue for attachments OVER the limit, and stores nothing (#5177)
    ↳ Error: Test timed out in 5000ms.

The other three queue workflows on that branch — Lint & Type Check, Spec Liveness Check, Governed Surface Guard — all succeeded.

Why it is not this PR's, by content

question reading
files this PR changes 8 — 2 changesets, packages/core/src/security/ ×3, packages/plugins/plugin-auth/ ×2, packages/rest/ ×1
of those, under packages/plugins/plugin-email/ 0
failure kind timeout, not AssertionError
assertion-shaped failures in the shard 0 — the one failed test is the timeout above

The triage rule this repo states applies directly: 「超时Test timed out in …)多半是负载/时序,不是本 PR 的回归」. Removing a zero-reader field from @objectstack/core cannot slow an email-attachment queue test in a package this diff never touches. ⭐ And note where the failure appeared: the PR's own CI is 41/41, 0 failing on the same head — the queue runs the full suite while PR CI runs the affected subset, so a queue-only red in an untouched package is the expected shape of this class, not a contradiction.

⭐ Second instance of #16434, in a different package

#16434 was filed hours ago from the same failure mode on a different suite: a test with no budget of its own running under vitest's default 5000 ms. It holds here too, measured on origin/main:

probe reading
testTimeout in packages/plugins/plugin-email/vitest.config.ts absent ⇒ default 5000 ms
per-test timeouts in email-service.queue-delivery.test.ts 0

⇒ two packages, two queue-only timeouts, same root shape. Recorded on #16434 rather than filed again. ⛔ Not fixed here — this PR is a published-type narrowing in @objectstack/core, and a test-config change in plugin-email would be an unrelated rider on it.

What happens next

A re-queue is owed — that is the single legitimate re-run for this class, and it is unspent. ⚠️ It cannot be issued right now: enable_pr_auto_merge is refused with "API rate limit already exceeded for user ID 19182527", a user-scoped budget disjoint from REST's (this comment went out over REST). ⛔ There is no substitute route — measured earlier today with controls: REST PATCH cannot un-draft or re-queue, and GraphQL is disabled for this session (a trivial control query returns the identical policy 403). The re-queue is scheduled and will be issued the moment that budget returns.

Nothing about this PR has changed: head 5ecf881fc6ac9b1a31fec37fda75b81058f21fc0 is unmoved, its CI is 41/41 with 0 failing, and git merge-tree against origin/main reports 0 conflict markers (35 commits behind — which the queue rebases, and is not a defect).


Generated by Claude Code

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Correction to the comment above: the re-queue is withdrawn, not merely delayed — and the cause is now diagnosed

domain:engine dispatching seat, 2026-09-07T05:5xZ.

What I got wrong

The previous comment said "A re-queue is owed — that is the single legitimate re-run for this class, and it is unspent", and treated the rate limit as the only thing standing between this PR and the queue. ⚠️ That was written before this seat had read the triage bot's own comment on this PR to the end. It names something that changes the answer:

src/email-service.queue-delivery.test.ts — 24h 内已弹出 2 个互相独立的 PR#16369#16442)。汇总 issue:#16506(新建)

and the checklist directly below it, item 2:

失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。

⇒ the "one legitimate re-run" rule is for a first, unexplained failure. This is the second independent ejection by the same test file in 24 hours, and the repo's own guidance says a re-queue before the fix just burns another full-queue round — at the cost of every PR behind it. ⛔ So this PR has not been re-queued, and will not be on this basis.

And the cause is no longer a guess

Measured and posted on #16506 (diagnosis): the failing test is a 650× outlier inside its own file — 648 ms while its 18 siblings run at 0–1 ms — and the whole cost is one assertion, a deep-equality walk over a 256 KiB Buffer inside toHaveBeenCalledWith(expect.objectContaining(...)). Isolated three ways: the allocation is 0 ms, the assertion is 679 ms, an identity assertion asserting the same property is 0 ms. At 648 ms idle it needs only ~7.7× contention to cross the 5000 ms default — which is why it ejects a PR now and then instead of always.

⇒ this is a removable cost, not a budget question, and the fix is one line in a package this PR does not touch (8 changed files, 0 under plugin-email).

Where that leaves this PR

Unchanged and correct: head 5ecf881fc6ac9b1a31fec37fda75b81058f21fc0, contract review PASS bound to that head, check-clause2-carriers --pair 16442 REAL_EXIT=0, own CI 41/41 with 0 failing, git merge-tree 0 conflict markers. Nothing here needs fixing.

⛔ What this seat will not do: skip, disable or quarantine that test; raise plugin-email's timeout; or carry any plugin-email change as a rider on a @objectstack/core published-type narrowing. The unblock is #16506's one-line fix landing, and that is now a decision for the maintainer — put to them directly along with this comment.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit b0529e1 Sep 7, 2026
42 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-14273-remove-dead-authrefusal-field branch September 7, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] ResolvedAuthzContext.authRefusal has zero transport readers — both #8287 refusal reasons collapse to the generic anonymous 401 on every wire

2 participants