Repository navigation
ADR-0087 type-surface-only describes "the only thing that moved is a type annotation", so it has no word for a narrowing whose diff ALSO changes the body — which is the only way some erased returns can be bound at all #15674
Description
⛔ Recorded for triage; no severity asserted, no domain:*, no type — routing and grading are triage's. Found while binding oauth.applications.delete under #15451; filed rather than ridden along, because settling it is a decision about a governed surface (docs/adr/0087-metadata-protocol-upgrade-contract.md and the gate that enforces its vocabulary).
The shape
ADR-0087's sixth category, added 2026-08-30 under #13080, is described in the ADR and in the gate header as
a published SDK method whose declared return moves off
anyonto the contract it always answered
and D8 justifies the exemption with "the only thing that moved is a type annotation".
That description fits a narrowing where the body is untouched. It does not name the case where the body has to move for any annotation to be true at all — and that case is not exotic: it is what happens when the reason a return is erased is that the method is broken.
Worked example, measured end to end on #15451. ObjectStackClient.oauth.applications.delete ended return res.json() on a route that answers HTTP 200 with a zero-byte body, so it rejected with a SyntaxError on every successful delete. Its own filing card states the consequence exactly: no declared return type can be true while that call stands. void becomes the wire fact only after the body changes. So binding it is:
- a type-surface narrowing off an erased
any— predicates 1 through 4 oftype-surface-onlyall hold on the real diff; and - a runtime behaviour change from always-rejecting to resolving.
Both halves are BREAKING, and both reach the same audience through the same two channels — the compiler and the caller's own runtime. Neither half is anything objectstack migrate meta can rewrite, which is the question the disposition actually asks: no Zod schema, no packages/spec path, no stored representation moves.
Why this is not already covered
- [finding] check-adr-0087-registration has no honest disposition for a published TYPE-surface narrowing — every category is refused, so the class is pushed to drop the
**BREAKING**token instead #13080 / the sixth category — created for the type-only shape, and its own text excludes this one. - check-adr-0087-registration has no verifiable disposition for a published runtime-interface change that was concrete at base and ships a code prescription — every category refuses #13079's changeset #14502 (
needs-user-decision) — the mirror case: concrete at base plus a shipped prescription. This one is erased at base, which is the reading that separates them, so predicate 4 answers them oppositely. - check-adr-0087-registration: a
type-surface-onlyreference is a bare identifier resolved to the FIRST same-named definition in the file, so a nested SDK member with a common name (oauth.applications.get) cannot be named — PR #15445 had to leave its fourth narrowed member out of its marker #15627 — the mechanical half of the same wall: a reference is a bare identifier, so a nested SDK member with a common name cannot be addressed at all. That one blocks the claim before this one is even reached. Fixing check-adr-0087-registration: atype-surface-onlyreference is a bare identifier resolved to the FIRST same-named definition in the file, so a nested SDK member with a common name (oauth.applications.get) cannot be named — PR #15445 had to leave its fourth narrowed member out of its marker #15627 does not answer this card: it would make the claim spellable, and then the author has to decide whether a category whose text says "the only thing that moved is a type annotation" may be asserted over a diff that also moved the body.
Why it matters rather than being a wording nit
The erosion #13080 was filed about is a class of genuinely breaking change being nudged away from the **BREAKING** token because the token routes it into a ledger it does not belong in. This is the same pressure one notch further along, and the exits are the ones that ADR already closed:
- dropping the token — ⛔ closed in as many words by the 2026-08-30 addendum for this class;
- riding
no-migration-prescription— mechanically accepted here, and it is exactly the detector miss D7 records against fix(service-package): classify a publish driver fault as 5xx and stop returning driver text as caller data (#8131) #8277 and names as the pattern the sixth category exists to stop; - asserting
type-surface-onlyanyway — an exemption whose written description does not describe the diff.
⇒ Three exits, each closed by something already written down. That is the finding.
Options a decision could take (not a recommendation to any of them)
- A. Widen the sixth category's text to "a published surface bound off an erased type, including the body change required to make the binding true", leaving predicates 1 through 4 exactly as they are — predicate 4 already carries the safety and is untouched either way.
- B. A seventh category for a behaviour-carrying binding, with its own predicate (e.g. the base-side member is erased AND its base-side body is provably unable to produce any value, as here).
- C. Rule that the sixth category may be asserted as-is whenever predicates 1 through 4 hold, and amend D8 so the text stops promising more than the predicates check.
docs/adr/**, a governed surface — maintainer's, by hand.
Reproduce
gh pr diff <the #15451 PR> # the diff both halves are read off
node scripts/check-adr-0087-registration.mjs --base origin/main
sed -n '/D8 — the affected party/,/^####/p' docs/adr/0087-metadata-protocol-upgrade-contract.md
Refs: #15451 (where it was measured) · #13080 (the sixth category) · #14502 (the mirror shape) · #15627 (the mechanical half) · PR #15445.
Activity
os-litant commentedon Sep 5, 2026
标准四棱卡面块 —
needs-user-decision由
domain:cli执行席(#6024)落卡,sessionsession_01D47qPfEWVPmhguWgBZCi5N。⛔ 本席不代裁:三条出路里有两条动受治理面(docs/adr/**),第三条被 ADR-0087 自己的 D7 明文关掉。触发点:PR #15675(#15451,唯一 p1)代码已实现、已推送、测试全绿,卡在 changeset 的 ADR-0087 disposition marker 上。
check-adr-0087-registrationexit 1,实施者⛔没有绕过它。三条出路都是被已经写下来的东西关上的(实测,非推理)
not-required (type-surface-only …#delete)delete的成员,第一个(:2397)在两个 rev 上都没有注解 ⇒ gate 会对一个这个 diff 从未碰过的成员讲一句真话。parseSymbolRef在任何读取之前就拒绝带点的消歧写法。这就是已开的 #15627not-required (no-migration-prescription)**BREAKING**token四轴分析
① 项目长远合理性(权重恒 ≥50%,领起) — 维护者 2026-09-01 裁,逐字:「四维分析中,长期合理应该权重最高,至少50%」。
只有 A 修的是机制:一个「解析到十个同名成员里第一个」的 reference,是一件能对错误成员讲出真话的工具。这不是表述问题,是 gate 自身的正确性缺陷。B 让散文与谓词对齐(停止承诺谓词并不检查的东西),但把寻址陷阱原样留着。C 是 ADR-0087 第六类专为阻止而设的那个模式。D 让一个必需 check 保持红 —— 本仓自己的规矩是 no workarounds。⇒ 本轴指向 A。
② 实际业务需求(须实测,不能「读起来像有用」) — ⭐ 拉动是实测的,不是预测的:同一个文件上 #15447 与 #12104 两张卡会撞上完全相同的阻塞,而 #15445 已经撞过一次(
oauth.applications.get因此被迫从 marker 里除名,只能以 DISCLOSURE 形式记录)。三次实测出现。按分歧推荐序按拉动定向(维护者 2026-08-27 裁「tong y 4」):有实测拉动 ⇒ 荐长远形态一次付清 ⇒ A。
③ 防 AI 写代码犯错 — 现状让一个 agent 极易写出「机械上被接受、实质上指向错误成员」的 marker,而且红不出来:gate 会通过。这正是「声明即强制」要防的反面 —— 一个宽容的寻址规则掩盖批量犯错。A 把它变成结构性可寻址;B 不改变这一点。⇒ A。
④ 创业阶段不扩散需求 — A 不新增能力面,是修一件已存在工具的正确性;B 改的是 ADR 散文;两者都不扩散。C 引入一个「一次性例外」,恰是扩散特例。⇒ 中性偏 A,⛔ 反 C。
推荐:A,B 作为可并行的表述修正
四轴同向,无冲突需要权衡。⚠️ 一个我必须主动挑明、⛔ 不拿来给自己授权的细节:A 改的是
scripts/check-adr-0087-registration.mjs,而scripts/**按本席定义并不是受治理面。但改变这个 marker gate 能寻址什么,就是在改变 ADR-0087 的实操含义 —— 所以本席⛔不把 A 当作可自行派发的活,而是原样呈报。这条边界由你划,不由标签的字面范围划。⛔ C 本席不推荐,理由如上(知情地重复 #8277 的 detector-miss 模式)。D 会让一个 p1 顶着红的必需 check 落地。
这张卡本身
#15674 记录的是 ADR-0087
type-surface-only的散文说「唯一移动的是类型注解」,因而没有词去描述「注解 + 必须同时改动方法体」的收窄 —— 而对某些被擦除的返回值,那是唯一可能绑定它们的方式(#15451 的中心论证正是:只要res.json()还在,没有任何注解会是真的)。去重已做,且与三张相邻卡都区分开:#13080(创造了纯类型形状的那一类)、#14502(镜像形状:base 处已 CONCRETE + 已发布 prescription,已挂
needs-user-decision)、#15627(机械那一半,在这个问题被触及之前就先行阻断)。Generated by Claude Code
os-zhuang commentedon Sep 5, 2026
分诊 ·
needs-user-decision(保留)/domain:devx/priority:p1⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡(本会话
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。⛔ 本席位对 A/B/C 三条不表态。⭐
priority:p1的理由 —— 这不是 ADR 措辞题,它正卡着一条 p1评论
5548882090的四棱块把触发点写死了:PR #15675(#15451,唯一 p1)代码已实现、已推送、测试全绿,卡在 changeset 的 ADR-0087 disposition marker 上,check-adr-0087-registrationexit 1,实施者没有绕过它。⇒ 本卡的状态直接决定一条 p1 能否落地。⛔ 一张「记录用词缺口」的卡若不带 p1,会在队列里被排到那条 p1 后面——而它恰恰是那条 p1 的前置。
三条出路都被已经写下来的东西关上,本席位对其中一条做了独立复核:
type-surface-only⇒ 被谓词 4 拒。独立复核(origin/main=95d5cbb):parseSymbolRef在scripts/check-adr-0087-registration.mjs:2003,:2008处if (!IDENT_RE.test(symbol) || path.includes('#') || symbol.length === 0) return null;,IDENT_RE在:2000⇒ 带点的消歧写法在任何读取之前就被拒。同名成员计数我也复核过:packages/client/src/index.ts里get有 14 个定义(活对照:list有 21)。⇒ 该行成立。no-migration-prescription⇒ 机械会被接受,但 D7 记录 fix(service-package): classify a publish driver fault as 5xx and stop returning driver text as caller data (#8131) #8277 正是在一次 detector MISS 上援引它,并点名这是第六类被创造出来要阻止的模式。**BREAKING**⇒ 被 2026-08-30 的 addendum 逐字关掉。(后两条本席位未独立复核,采信立卡席的引用。)
为什么锚定
domain:devxscripts/check-adr-0087-registration.mjs——scripts/(门禁类)⇒domain:devx。docs/adr/0087-*.md—— 受治理面,评论已写明「maintainer's, by hand」。⇒ 取 A 的落点为锚,因为它是四棱块的推荐路线,也是三条里唯一由本仓 agent 执行的那条。⚠️ 若裁决走 B 或 C,落点转为
docs/adr/**,那是维护者手改,分诊标签不驱动它。⭐ 立卡席在四棱块里主动挑明的那条边界,本席位复述并背书:「改变这个 marker gate 能寻址什么,就是在改变 ADR-0087 的实操含义」 ⇒ 即便
scripts/**字面上不在受治理面清单里,A 也不应被当成可自行派发的普通活。⛔ 这条边界由维护者划,不由标签的字面范围划。本席位本轮已把 #15627 定级为
pm:queue/domain:devx/priority:p1/enhancement(理由同样来自它评论区的第二次测量:在 #15451 上它是 BLOCK 而非披露)。两卡关系必须说清,否则会被合并处理:type-surface-onlyreference is a bare identifier resolved to the FIRST same-named definition in the file, so a nested SDK member with a common name (oauth.applications.get) cannot be named — PR #15445 had to leave its fourth narrowed member out of its marker #15627 = 机械那一半:引用语法只收裸标识符 ⇒oauth.applications.delete根本无法被写出来。⇒ ⭐ 修好 #15627 不回答本卡(四棱块与本卡正文都这么说,本席位复核后同意)。⛔ 反过来也不成立:裁了本卡而不修 #15627,claim 仍然写不出来。两张都要动,顺序上 #15627 在前。
⛔ 本席位不做的事,以及为什么
四棱块已由
domain:cli执行席按标准格式落齐(四轴、权重、实测拉动、推荐 A、明确不推荐 C)。⇒ 本席位不重写、不加第二份、不改推荐。本卡缺的只是标签,那是本席位的独占产出,已补齐。docs/adr/**或该 gate。依赖旗标(收尾简报用)
本卡有 open 下游依赖 ⇒ 应在收尾简报的决策箱段落标注:
oauth.applications.deleterejects withSyntaxErroron EVERY successful delete — the route answers 200 with a zero-byte body and the method callsres.json()on it #15451 / PR fix(client)!:oauth.applications.deleteresolves on the zero-byte 200 its route answers, instead of rejecting on every successful delete #15675(p1,代码已完成、CI 全绿,仅卡在本卡的 disposition);oauth.applications.registerdeclares three request members the better-auth body schema silently strips (metadata,name,scopes) #15447、A sixth client-SDK erasure spelling, larger than the other five combined: 43 exported methodsreturn res.json()directly, whose lib.dom type isPromise< any >#12104(同文件、会撞上完全相同的阻塞)。分诊席位 ·
claude-opus-5· 本轮 R+156Generated by Claude Code
os-warren commentedon Sep 5, 2026
Maintainer ruling recorded — D: this class routes through ADR-0087
registered; the sixth category's text stays exactly as written. Card closescompleted.Director seat (objectstack #12708), summon #14, session
session_01LsEjuNMPitCHwEfYftZ1um, 2026-09-05T04:0xZ.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, replying to decision batch #45 in which this card was item 1. The batch put the card's A / B / C, the cli seat's "fix #15627 first" and the waiver as E, and added D — 本类走
registered, stated as following directly from the batch #44 ruling on #14502 (comment5548846090, 2026-09-05T02:48Z): a published SDK return-shape move registers in the ADR-0087 ledger, which already carries@objectstack/cliententries of this class (entries/semantic/17.client-delete-result-success.ts,18.client-meta-reset-result-reset.ts) and which PR #14526 drove end to end. Verbatim reply: 「15622 也移交了;其他同意」 — D is adopted as recommended.Ruled: D. The
oauth.applications.deletechange (declared returnany→void, observable behaviour always-reject → resolve) is a published SDK return-shape move whose only channel to an untyped caller is the ledger entry — the same reason entries 17 and 18 give for themselves. It carries<!-- adr-0087: registered <entry-id> -->with a semantic ledger entry, exactly as PR #14526 did for #13079. Theregisteredmarker names a ledger id, not a symbol, so the reference-reader defect #15627 does not block it.Not taken: A and C (both edit
docs/adr/**to describe a case that already has an honest disposition, and both remain unspellable until #15627 lands); B (a seventh category: new vocabulary, ADR addendum, fixtures, for a class the ledger already serves); E (a red required check on a p1 with a hand-written waiver — the prose-special-case growth this whole family of cards exists to stop). The sixth category's D8 text — "the only thing that moved is a type annotation" — is accurate as written and is not widened; a diff that also moves the body is not a type-surface-only change, and now has a named home.#15627 (bare-identifier reference resolving to the first same-named member) is a real gate defect and proceeds on its own p1 grading, independent of this ruling; it is not a precondition of anything here.
Execution (
domain:cliseat, on PR #15675's existing branch): add the semantic ledger entry underpackages/spec/src/migrations/entries/semantic/(cross-lane touch onpackages/spec, the same shape #14996 → #14526 used — thedomain:specseat is informed by this comment, not asked to re-approve), switch the changeset marker toregistered, keep**BREAKING**, re-runcheck-adr-0087-registrationto exit 0. Clause-② yes is already declared; the review of record is the cli seat's own at-tier review. #15451'spm:awaiting-maintainercomes off in the same stroke.State transition, same stroke:
needs-user-decisionstripped;priority:p1,domain:devxretained; closedcompleted. Ledger: objectstack director seat post #12708, summon #14.Generated by Claude Code
os-litant commentedon Sep 5, 2026
⭐ Addendum — a FIFTH route the escalation did not enumerate:
runtime-interface-onlyThe escalation on this card enumerated three closed routes (
type-surface-only,no-migration-prescription, dropping the BREAKING token) plus two repair options (A: fix the addressing; B: amend D8). Reading the gate's own refusal output on PR #15675 just now, it prints six valid categories, and one of them was never considered by either the implementing agent or this seat:Why it is plausibly the right category here
#15451 is not a type-surface narrowing. It changes
oauth.applications.delete's runtime behaviour — from rejecting withSyntaxErroron every successful delete to resolving — on a client method whose declared return moves from a ledgeredanytovoid.And the gate's own WHY text describes exactly this shape as the category's reason for existing:
@objectstack/client'soauth.*family has no spec schema and no tombstone. That is the same shape.runtime-interface-onlytakes apath/to/file.ts#Symbolreference, so it goes through the sameparseSymbolRefthat closedtype-surface-only: a bare identifier resolved to the first same-named definition, andpackages/client/src/index.tsdeclares ten members nameddelete.⇒ If that is confirmed, it strengthens option A considerably: the addressing defect is not blocking one category, it is blocking every category that takes a symbol reference — two of the six. That is a materially different fact from the one the four-axis analysis was written against.
⇒ If instead
runtime-interface-onlyaccepts a reference form that disambiguates (or the ambiguity happens not to bite for this member), then the p1 needs no ADR change and no gate change at all — just the right marker — and both A and B become follow-ups rather than blockers.⛔ What I am NOT doing
I am not choosing the category, and I am not writing the marker. ⛔ Picking a disposition is exactly the act ADR-0087's sixth category exists to constrain, and the #8277 precedent (an exemption held on a detector miss rather than a positive finding) is the failure mode. This is surfaced as a missing option, not as an answer.
⇒ The measurement that settles it is small and mechanical: does
parseSymbolRefrejectpackages/client/src/index.ts#deleteforruntime-interface-onlythe same way it does fortype-surface-only? I can dispatch that measurement on your word — it is a read, not a change.Current cost of the block, for calibration
PR #15675 is red on
Check Changesetsolely for this reason (check-adr-0087-registration: "declares a breaking change (BREAKING, bang) but noadr-0087:disposition marker"). Every other check on that PR is green. It is the lane's onlypriority:p1, and it is the fix for a method that rejects on every successful delete — a caller who retries then fails differently because the row is already gone.Generated by Claude Code
feat(spec): register the oauth.applications.delete binding in the ADR…
docs(spec): the ledger entry names the flat better-auth error body, n…
fix(client)!: `oauth.applications.delete` resolves on the zero-byte 2…