Repository navigation
Description
Found while implementing #15587; filed unassigned, pm:queue only, for triage to grade and route.
Measured
packages/plugins/plugin-auth/src/auth-manager.ts now raises an explicit refusal on /sign-up/email using better-auth's own constant:
const { APIError, BASE_ERROR_CODES } = await import('@better-auth/core/error');
throw APIError.from('UNPROCESSABLE_ENTITY', BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);That puts USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL on the wire from our own code. Neither vocabulary gate can see it. Controlled measurement, not inference — the ledger entry was removed from disk (mutation confirmed by counting the line off the file and by a distinct git hash-object), and the gate was re-run:
ledger entry occurrences before=1 after=0
mutation CONFIRMED on disk
CONTROL check:dispatcher-error-vocabulary EXIT=0
Exit 0 with the registration absent. A gate that would have caught an unregistered emitted code must go red there; this one stays green, so its green carries no information about this stamping shape. check:error-code-provenance agrees by its own printed bounds — patterns = objlit, assign, constdef, "blind to non-*_CODE constants, helper/shorthand indirections, templates and class fields".
Why this is not #9223
Closed #9223 covered a non-literal at an object-literal code: key, and the gate has since grown objlitconst / objlithelper / objlitexpr for exactly that. This shape is different on both axes: the value sits in an argument position of APIError.from(status, rawError), and it is a member expression on an imported third-party object, so there is no code: anchor to find and no in-repo constant to resolve.
Why it matters
ADR-0112's premise is that an unregistered code fails schema parse and therefore fails CI — "no silent fourth state". For this shape the enforcement is absent, and the shape is not exotic: APIError.from(status, BASE_ERROR_CODES.X) is better-auth's own documented idiom, and plugin-auth is wired to that vendor throughout. USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL had in fact been reaching the wire unregistered from the vendor's own invite_only lane before #15587 registered it — nothing reported that in either direction.
Note the registration is still correct and is included in #15587; what is missing is anything that would have required it, or that will require the next one.
Suggested direction (not a decision)
Reach the APIError.from / new APIError argument position, at least for a member expression whose object is an imported binding the scan can name. Whether vendor-owned code tables should be registered wholesale or per emitted member is the real question and belongs to triage — registering a vendor's entire table would import a vocabulary this repo does not own.
Refs: #15587 (where this surfaced) · #9223 (the closed, adjacent object-literal case).
Activity
os-zhuang commentedon Sep 5, 2026
分诊 ·
pm:queue/domain:devx/priority:p2/enhancement/finding⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。
⭐ 复核时测出一件比卡片更强的事实:这个形状今天已经活在
main上卡片以 #15587 的新 emit 为例,而那条 emit 尚未落地 —— 实测(
origin/main=6c08131):⇒
APIError+BASE_ERROR_CODES的组合在main上已经是活代码(admin-impersonate-endpoint.ts),⛔ 不是一个等 #15587 才会出现的假想形状。⇒ 本卡现在就可派发,不必等那条 PR。⭐ 卡片自己的对照做法也值得记一笔:它把 ledger 条目从磁盘上删掉(并以行数与
git hash-object双重确认变异真的落盘),再重跑门 ⇒EXIT=0。一道本该抓住未注册 emit 的门,在注册缺席时仍然绿 —— 这不是推理,是受控测量。这正是本班次反复要求的「消融证明断言非空跑」。为什么锚定
domain:devx(并点名另外两处落点)本卡实际横跨三个面,复核后逐一定位:
scripts/check-dispatcher-error-vocabulary.mjsdomain:devx← 主锚packages/spec/scripts/check-error-code-provenance.tsdomain:specpackages/runtime/src/dispatcher-error-vocabulary.tsdomain:cli⇒ 主锚取被受控测量证明为盲的那一道。⚠️ 若修复需要同时动
packages/spec那道门,那是跨车道,⛔ 请由派发侧按流程处理,不要在本卡的 PR 里顺手扩面。priority:p2的理由ADR-0112 的前提在这个形状上没有执行力:其premise 是「未注册的 code 会 schema parse 失败、因而 CI 失败 —— 没有静默的第四态」。而这里恰恰有一个静默的第四态。
加重的两点:
APIError.from(status, BASE_ERROR_CODES.X)是 better-auth 自己文档化的习语,而 plugin-auth 整体接在这个供应商上(APIError55 行)。USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL此前就经供应商自己的invite_only通道未注册地上过线,两个方向都没有任何东西报告。不给 p1:没有错误的响应体、没有权限后果;坏的是执行力而不是行为,且 #15587 已把那一个具体注册补上。
⭐ 卡片留给分诊的那个真问题 —— 本席位给判据,⛔ 不代裁
⛔ 本席位不选,只给一条判据,因为卡片已经把它的代价说出来了:整表注册会把一份本仓并不拥有的词汇表引进来。⇒ 请在动手前明写选了哪一种;若选整表,需要回答「供应商升级新增一个 code 时,本仓的词表是自动跟随还是变红」——这两种答案是不同的产品。
needs:contract-review。⛔ 边界
⛔ 不要把本卡读成 #9223 的重复。 卡片把两条轴都划清了,本席位复核后同意:#9223 是对象字面量
code:键上的非字面量(门此后长出了objlitconst/objlithelper/objlitexpr);本卡的值在APIError.from(status, rawError)的实参位置,且是一个 import 进来的第三方对象上的成员表达式 ⇒ 既没有code:锚点可找,也没有仓内常量可解析。Refs:#15587(本卡浮现处;注册本身是对的且已含在其中)· #9223(已关,相邻的对象字面量情形)。
分诊席位 ·
claude-opus-5· 本轮 R+158Generated by Claude Code
claude commentedon Sep 5, 2026
Re-graded →
pm:awaiting-maintainer(decision), by the PM seatdomain:devx(session012zGPuVVX3deAx9LdjK8jCk). Triage is right that the card's real question sits on the ACCEPT SET (which codes count as legitimately on the wire), and that is not the seat's to rule.The decision: a vendor-owned code table (
BASE_ERROR_CODESfrom@better-auth/core/error) reaches the wire throughAPIError.from(status, BASE_ERROR_CODES.X)— better-auth's own idiom, live on main atadmin-impersonate-endpoint.ts— and no gate requires the emitted member to be registered in the repo's vocabulary. How should such codes be registered?APIError.from/new APIErrorargument position for a member expression on an imported binding, and each emitted vendor member must be registered inpackages/runtime/src/dispatcher-error-vocabulary.ts(as #15587 did forUSER_ALREADY_EXISTS_USE_ANOTHER_EMAIL)BASE_ERROR_CODESinto the vocabulary)invite_only-style lanesSeat recommendation: A now, C as the follow-up if the vendor-lane hole is judged worth closing. B is refused on the card's own cost. Mechanism note for whoever executes: the scanner change is in
scripts/check-dispatcher-error-vocabulary.mjs(domain:devx);packages/spec/scripts/check-error-code-provenance.tsstates its own bounds and isdomain:spec's — not to be widened in the same PR. Entered in seat post #6023's decision box; not nudged.Generated by Claude Code
huangyiirene commentedon Sep 9, 2026
状态转换:
pm:awaiting-maintainer→pm:queue(2026-09-09)维护者回批逐字:「C 桶那 24 张的批量转
pm:queue同意」。判据:⭐ 本卡从来就不该离开队列。卡面首句自陈「Found while implementing #15587; filed unassigned,
pm:queueonly, for triage to grade and route」,且把真问题明确交给分诊席而非维护者:「Whether vendor-owned code tables should be registered wholesale or per emitted member is the real question and belongs to triage」。⇒ 它在某一步被误改成 awaiting,本笔改回。⛔ 不是维护者面:新增门禁覆盖是门禁加强(人工地板管的是门禁削弱:降阈值、删必查项、抬 ratchet 上限、跳过测试)。
enhancement、domain:devx、finding、priority:p2留下。Generated by Claude Code
os-justin commentedon Sep 10, 2026
Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session
session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: #17285).Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on #17285), the contract-review tier is reserved for the skills seat (protocol files + the published
skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.For this card: the contract-review-tier build or review its thread asks for is no longer required. The lane seat's own default-tier review, plus the gates (widening tells, pin tests,
dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: theClause-②declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touchingpackages/specgoes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.Generated by Claude Code
claude commentedon Sep 14, 2026
Claim: PM loop round 7
Session:
session_017ef78bLdybu3AffehKkhfkBranch:
claude/issue-15723-apierror-argument-positionWorktree:
objectstack-issue-15723Domain:
domain:devxFile surface:
scripts/check-dispatcher-error-vocabulary.mjs(+ its self-test) and, only if an emitted vendor member proves unregistered,packages/runtime/src/dispatcher-error-vocabulary.ts. ⛔packages/spec/scripts/check-error-code-provenance.tsis OUT — it states its own bounds and isdomain:spec's; triage said in as many words ⛔ 不要在本卡的 PR 里顺手扩面 (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate — floor sonnet · default opus · ceiling fable; default tier is the review of record here since the contract-review-tier requirement on this card was LIFTED, comment 5612083976)Clause-②: no
Thread-read: 5612083976
Serial constraints cleared: ⭐ THE FORK IS SETTLED FOR THIS CARD AND THE SEAT IS SCOPING IT TO OPTION A ONLY — the scanner learns the
APIError.from/new APIErrorargument position for a member expression on an imported binding, and each emitted vendor member must be registered. ⛔ Option B (register the vendor table wholesale) is REFUSED on the card's own stated cost and would widen the accept set — if the work turns out to require B, STOP and report, ⛔ never changeClause-②yourself. ⛔ Option C (a declared audited allow-list reaching the vendor's owninvite_onlylanes) is a FOLLOW-UP, not this card. Basis: comment 5595053798 returned the card topm:queueon the maintainer's verbatim batch ruling and recorded ⛔ 不是维护者面:新增门禁覆盖是门禁加强(人工地板管的是门禁削弱); comment 5550384097 is the seat analysis recommending A. No in-flight claim names this scanner; #15587 (where the shape surfaced, and which registered the one code) is landed.domain:devx执行席 · 座位贴 #6023 · claim written at 2026-09-14T17:14ZGenerated by Claude Code
claude commentedon Sep 14, 2026
os-dev-report
{ "issue": 15723, "status": "done", "branch": "claude/issue-15723-apierror-argument-position", "pr": "https://github.com/objectstack-ai/objectstack/pull/18222", "premise_still_valid": true, "summary": "Option A only, as scoped. scripts/check-dispatcher-error-vocabulary.mjs grows a fifth stamp shape, `apierrorarg`: the argument position of `APIError.from(status, record)` / `new APIError(status, record)`, where the record is a member expression on a binding the file imports and the member NAME is the code. Four published guards (argument one only; member expression only; object must be an imported binding; member must be in ADR-0112 D1's value space), the last two failing LOUD as unresolved reason `vendor-member` rather than silently, so an import spelling the recogniser does not know reds the gate instead of hiding a code. Option B refused on the card's own cost, option C left as the follow-up, and the refusal is written into the header and the printed bounds so it is collectable. `packages/runtime/src/dispatcher-error-vocabulary.ts` was NOT touched: the one live emitted vendor member is already registered (by #15587), so the widening derives 0 sites, 0 verdict rows and 0 unresolved today and the scanned site total is unchanged at 52. No existing pattern relaxed, no self-test case removed. Assignee field arrived set to the maintainer account by the PM dispatch (step 1) with the dispatch Claim comment 5667828903 naming this branch; not written by me.", "tests": "ABLATION, both directions, same script, same mutation, from the COMMITTED fix (HEAD 665a04a07, BASE 2d3d1c969). Mutation = delete the ledger registration line from packages/spec/src/api/error-code-ledger.zod.ts on disk; confirmed by occurrence count AND a distinct git hash-object; restore proven by blob equality with the HEAD blob plus an empty `git diff HEAD`, never by an exit code; trap restores on INT/TERM with an absolute path. BEFORE (on origin/main, no fix): `before hash c19c00ba... -> after hash e2f9f960...`, occurrences 1 -> 0, `MUTATED check:dispatcher-error-vocabulary EXIT=0`, gate output mentions the code 0 times -- the card's baseline reproduced. AFTER (fix committed): identical mutation, `EXIT=1`, output mentions the code 3 times, finding `[unclassified-site] packages/plugins/plugin-auth/src/auth-manager.ts stamps unregistered code 'USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL' (apierrorarg) ...`, sites 52 -> 53. REVERSE direction: registration restored (hash == HEAD blob, git diff HEAD empty) -> `EXIT=0`, 52 sites, no finding. | `pnpm check:dispatcher-error-vocabulary` :: exit 0 -- self-test `11 shapes + 396 assertions OK` (was 10 shapes + 352). New battery `[#15723] THE APIError ARGUMENT POSITION: the record is the stamp.` floor 44; verified it registers exactly 44 by temporarily pinning the floor to 999 and reading the count the failure prints. | GATES: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 32 families; all 32 run, each exit code captured BEFORE any pipe; `--ran` reconciliation prints `32 derived, 32 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero -- all 32 recorded an exit code and none of them is 3)`. All 32 exit 0. | `pnpm lint` (repo-wide `eslint . --no-inline-config`) :: exit 0 -- run in full, so no narrowing to declare. | EXTRA (dispatch-gates flags them as keeping a roster under scripts/, my edited directory): check-published-list-mirrors + its self-test, check:console-injection, check:engine-double-contract, check:i18n-stale-fill, check-skills-token-ratchet + its self-test -- all exit 0. check:error-code-casing (whose findViolations this gate imports) and @objectstack/spec check:error-code-provenance -- both exit 0, unchanged. | packages/spec error-code-ledger.test.ts :: 20 passed, under the shared verify lock (OS_VERIFY_LOCK_SLOT=issue-15723-ledger-test, VERDICT command-exit 0, waited 0s) -- the ablated-and-restored ledger file is byte-identical to HEAD. | NOT MEASURED: check:published-readme-exports -- exit 3, PREREQUISITE NOT MET (40 packages have no built dist/*.d.ts in this worktree). Not one of the 32 derived families, and its population is untouched by construction: the diff edits no package source, no README and nothing inside any package's files[]. | LOCAL SCOPE: tiers 1 and 2 are EMPTY by rule -- the diff touches no package (one repo-root scripts/ file), so there is no dependency closure to build and no affected package's test/typecheck to run. | CHANGESET: none, measured not assumed -- the only edited path is repo-root scripts/, the root manifest is private: true, and no published manifest's files[] escapes its own directory, so nothing published moves. `skip-changeset` applied additively (POST .../issues/18222/labels, HTTP 200) and read back comparatively: read-back ['size/l','skip-changeset'], union(read, target) minus read-back = NONE, nothing stripped.", "mcp_calls": "0 -- no MCP GitHub tool was called at any point in this run", "api_writes": "3 -- POST /repos/objectstack-ai/objectstack/pulls (draft PR #18222); POST /repos/objectstack-ai/objectstack/issues/18222/labels (skip-changeset, additive); POST /repos/objectstack-ai/objectstack/issues/15723/comments (this report). No PATCH of any body. Plus 2 git pushes on the branch (the empty-branch routing probe, then the commit).", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: packages/spec/scripts/check-error-code-provenance.ts is blind to this same argument position by its own printed bounds (patterns = objlit, assign, constdef). Out of this card's file surface by triage's explicit instruction; it is domain:spec's. Successor: the seat that widens that gate on its own card.", "noted, not filed: the vendor's OWN lanes (better-auth emitting a BASE_ERROR_CODES member from inside its routes, e.g. the invite_only lane the card measured) are written in no source in this repo, so no source scan reaches them. This is option C, already ruled a follow-up, and it is now recorded in the gate header and in the bound the gate prints on every run. Successor: option C's card.", "noted, not filed: importedBindings reads the FILE, not lexical scopes, and knows three import spellings. Both are declared bounds with self-test fixtures, and a spelling it misses reds the gate (unresolved) rather than hiding a code, so there is nothing latent to file. Successor: none -- the failure direction is loud." ] }Generated by Claude Code
claude commentedon Sep 14, 2026
复核裁决:ACCEPT(实质已过) —— 我把两个方向的消融自己重跑了一遍,不是采信报告
domain:devx执行席 · 座位贴 #6023 · PR head665a04a07,base2d3d1c969· 取数时刻 2026-09-14T18:04Z一、⭐ 独立复现:同一棵被变异的树,旧门禁绿、新门禁红
我在 PR head 的一次性 worktree 里,把
USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL的注册行从磁盘上删掉,然后在同一棵树上分别跑新旧两个版本的门禁:⛔ 复位不靠退出码,靠
git hash-object相等 + 空git diff HEAD。两个方向都动了,而且旧门禁在完全相同的变异下保持绿 —— 这就是本卡要修的东西,现在有一条会红的路。c19c00ba… → e2f9f960…,与我独立跑出来的逐字节相同。二、dev 没有照抄卡面,而且是对的
卡片正文把
packages/runtime/src/dispatcher-error-vocabulary.ts称作词表本体,但这个 code 的实际注册在packages/spec/src/api/error-code-ledger.zod.ts:572。门禁自己读三个文件(LEDGER_ZOD/ERRORS_ZOD/DECLARATION,:340-342),dev 变异的是真正承载注册的那一个。⇒ 若照卡面去删,消融根本不会红,整条证据链会塌。三、Option A 的边界守住了
scripts/check-dispatcher-error-vocabulary.mjs(+578/−3)。packages/runtime/src/dispatcher-error-vocabulary.ts没被碰 —— 唯一那个活的 vendor 成员 plugin-auth: a sign-up for an address that already has asys_userrow answers 200 and persists nothing (postureemail_domain) #15587 已经注册过了,所以今天不欠注册。packages/spec/scripts/check-error-code-provenance.ts没被碰 —— 分诊席原话就是不要在本卡 PR 里顺手扩面。vendor-memberunresolved 使门禁变红,⛔ 不是静默放过。这个方向选对了 —— 反过来会再造一个静默第四态。四、自检
origin/main落后二十余个提交,所以「原来是 351 还是 352」不可比;可比的是 +1 个 shape、断言数大幅增加,这一点两边一致。新 battery 声明了 floor 44,dev 报称把 floor 临时钉到 999 读失败信息里的真实计数来验证它确实注册了 44 条 —— ⛔ 这一步我没有重跑。五、入队前检
Fixes #15723第 0 行 · 逐字节Clause-②: no恰一行 ·check-clause2-carriers --pair 18222✓ ·skip-changeset成立(唯一改动路径是仓根scripts/,根 manifestprivate: true,没有任何已发布 manifest 的files[]越出自己目录)。绿判据按 check 名去重取最新:33 次运行 → 29 名 → 仍有 3 个 in_progress(
Lint & Repo Gates、Test Core (1/6)、Type Check · workspace)⇒ 现在不翻 ready、不武装,全绿才动。六、三条越界发现,dev 都标了 noted-not-filed,我同意不立卡
check-error-code-provenance.ts对同一实参位置也盲 —— 那是domain:spec的,归它自己的卡;vendor 自家通道(invite_only)在本仓没有源码,任何源码扫描都够不到 —— 那就是 option C,已裁为后续,而且现在被写进了门禁每次运行都打印的边界里,不会丢;importedBindings读文件不读词法作用域、只认三种 import 拼法 —— 是已声明的边界,有 self-test fixture,且漏掉的拼法使门禁变红而不是藏起一个 code ⇒ 没有潜伏物可立。Generated by Claude Code
claude commentedon Sep 14, 2026
✅ 已落地 ——
2445b27cbonorigin/maindomain:devx执行席 · 座位贴 #6023 · 落地两读,⛔ 不看merged布尔、⛔ 不看 head-sha 祖先性 · 取数时刻 2026-09-14T19:03Z读数一 ·
main内容探针读数二 ·
main提交图合并 19:01:54Z。
Fixes #15723已代关本卡;pm:dispatched在同一动作里摘除,读回enhancement, priority:p2, domain:devx。这张卡的证据是我自己重跑出来的,不是采信报告:在 PR head 的一次性 worktree 里删掉注册行,在同一棵被变异的树上分别跑新旧两版门禁 —— 旧门禁
EXIT=0且提及该 code 0 次(卡片测到的静默第四态原样复现),新门禁EXIT=1并报出[unclassified-site] … (apierrorarg);复位后新门禁EXIT=0。复位以git hash-object相等 + 空git diff HEAD证明,⛔ 不靠退出码。报告给的两个 blob 哈希与我独立跑出的逐字节相同。范围守住了:只动一个文件;
packages/runtime/src/dispatcher-error-vocabulary.ts没碰(唯一活着的 vendor 成员 #15587 已注册);⛔packages/spec/scripts/check-error-code-provenance.ts没碰(分诊席原话)。Option B 被拒、C 留作后续,而且拒绝被写进了门禁每次运行都打印的边界里 —— 比写在 PR 正文里更不易丢。本卡关闭,无遗留。
Generated by Claude Code
feat(devx): the dispatcher vocabulary gate learns the APIError argume…