Skip to content

auth-mount coverage — follow-ups 1/2/4 LANDED; remaining scope is follow-up 3 (sdk bridge) + the #10975 ledger row (body below is the original census, now historical) #10534

Description

@os-zhuang

#10050 asked whether POST /organization/add-member being mounted-but-undocumented was a one-off. It was not. Census below, measured on origin/main at 4544be260, with positive controls on both legs.

Population

auth-plugin.ts mounts routes directly on the raw Hono app, ahead of the better-auth catch-all. Those mounts are the ObjectStack-owned auth surface — the catch-all never sees them, so the vendor's own route table cannot account for them.

Enumerated by matching rawApp.<verb>(\${basePath}/…`)inpackages/plugins/plugin-auth/src/auth-plugin.ts(excluding therawApp.allcatch-all and the.well-known/*mounts, which are not underbasePath`): 17 routes.

Leg 1 — documented?

Test: does the literal wire path /api/v1/auth<path> appear anywhere in the hand-written docs corpus (392 .md/.mdx files under content/docs, content/docs/releases/** excluded as release-owned)?

6 of 17 undocumented, before this card's PR:

Route In a ledger?
POST /api/v1/auth/admin/sso/request-domain-verification no
POST /api/v1/auth/admin/sso/verify-domain no
POST /api/v1/auth/admin/unban-user vendor inventory only
GET /api/v1/auth/bootstrap-status yes — objectstack / sdk
POST /api/v1/auth/organization/add-member yes — objectstack / server-only
POST /api/v1/auth/set-initial-password no

#10050's PR documents the last-but-one, leaving 5 of 17.

⚠️ Two refinements the raw number hides. set-initial-password is named once in prose (authentication.mdx, "offers set-initial-password") without its URL — named, not documented. And add-member was named three times in tenancy-modes.mdx with no URL anywhere, which is how it stayed invisible while looking discussed.

Positive control (leg 1): the same expression, run before the docs were written, returned organization/add-member — the known-present member of the set. So a zero would have been a real zero. Control output: POSITIVE CONTROL organization/add-member present in undocumented set: true.

Leg 2 — ledgered?

auth-route-ledger.ts has two halves: the reviewed AUTH_ROUTE_LEDGER rows and the machine-maintained BETTER_AUTH_MOUNTED_SURFACE inventory. A route in neither is unaccounted for.

9 of 17 appear in neither half:

/admin/import-users · /admin/oauth2/toggle-disabled · /admin/sso/register · /admin/sso/register-saml · /admin/sso/request-domain-verification · /admin/sso/verify-domain · /admin/unlock-user · /set-initial-password · /sys-oauth-application/register

Only 3 of the 17 carry a reviewed source: 'objectstack' row: /config, /bootstrap-status, /organization/add-member. The rest are either attributed to the vendor (correct where the vendor also declares the path — the ObjectStack mount shadows it) or absent entirely.

Verified two ways that agree: a regex parse of the ledger source, and a direct grep "auth<path>'" per route. Positive control (leg 2): the same grep returns 1 for /organization/add-member and 1 for /config, so an absence is an absence and not a broken pattern.

Leg 3 — why nothing caught it

Both of #10050's asks land on the same hole, and it is upstream of either ledger row.

scripts/docs-audit/affected-docs.mjs --bridge-coverage on this tree:

  client-bound ledger rows ... 221
    reachable ................ 45
    UNREACHABLE .............. 176
        55 of 55   unreachable  packages/plugins/plugin-auth/src/auth-route-ledger.ts

Every client-bound row in the auth ledger is unreachable by the sdk doc-nomination bridge. The bridge finds route registrars by filename convention — REGISTRAR_FILE_RE = /(?:^|\/)(?:[\w.-]*route[\w.-]*|[\w.-]*-server)\.ts$/ — and the file that actually registers this surface is auth-plugin.ts, which matches neither alternative. No registrar tail is produced for the auth surface, so no auth ledger row can be selected, so the sdk anchor never nominates a doc for an auth route.

add-member is then invisible a second time on top of that: its row is disposition: 'server-only' with no client, so it is not even among the 55 client-bound rows the bridge fails to reach.

The 221→45 shape is already recorded in #9572 (closed). What is new here is the auth slice being 55 of 55 and the concrete cause being one filename convention — plus the demonstration that this is what let a route mount and its documentation gap ship separately with nothing objecting.

⛔ Deliberately not acted on: scripts/docs-audit/affected-docs.mjs is being edited by open PR #10501, which is fixing a different silent-parse defect in the same file. Any remedy here belongs after that lands.

Suggested follow-ups

  1. Document the 5 remaining undocumented mounts (or record a deliberate "server-only, not user-facing" verdict for each — bootstrap-status may well be one).
  2. Ledger the 9 unledgered mounts as source: 'objectstack' rows, which is what the ledger's own header says objectstack mounts are for.
  3. Decide whether the sdk bridge should reach the auth surface at all — either by teaching REGISTRAR_FILE_RE about auth-plugin.ts, or by declaring registrars explicitly rather than by filename convention. After fix(docs-audit): a PARTIAL ledger read is a verdict, not a smaller number #10501.
  4. Consider a gate: a rawApp mount under basePath with no ledger row is exactly the state that produced [finding] sys_member "Add Member" action targets POST /organization/add-member, which better-auth 1.7.0-rc.2 never mounts (server-only) — on multi-org there is NO remaining UI path to attach an existing user to an org #9941 and docs: the newly mounted POST /organization/add-member is undocumented — and it is the ONLY multi-org path to attach an existing user #10050, and it is mechanically detectable.

Refs

#10050 (the card this sweep came from) · #9941 / PR #10049 (the mount) · #9572 (the 221→45 bridge measurement, closed) · #10501 (open, owns affected-docs.mjs) · #9969 / #9652 (adjacent: vendor /admin/ routes and their admit sets)

Activity

  1. added theissue type on Aug 21, 2026
  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    Triage (first-touch grading): promoted finding → pm:queue, domain:services (landing surface: packages/plugins/plugin-auth/src/auth-route-ledger.ts + content/docs route pages), type Task.

    Scope ruling: follow-ups 1 + 2 only — document the 5 remaining undocumented mounts (or record a per-route "server-only, not user-facing" verdict where that's the truth, e.g. bootstrap-status candidates) and ledger the 9 unledgered mounts as source: 'objectstack' rows. Both are exactly what the ledger's own header declares itself for; the census with positive controls on both legs is dispatch-ready evidence.

    Fenced out of this card: follow-up 3 (the REGISTRAR_FILE_RE / bridge fix) — scripts/docs-audit/affected-docs.mjs is open PR #10501's surface; file it as its own card after #10501 lands. Follow-up 4 (a mount-without-ledger-row gate) — worthwhile, file separately once 1+2 establish the clean baseline it would ratchet.


    Generated by Claude Code

  3. self-assigned this
    on Aug 21, 2026
  4. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    Claim: PM domain:services 派发(范围 = 建议 1 + 2,见下)

    • Session: 0f14f70b-575c-5f2b-a235-4000a55db042
    • Branch: claude/issue-10534-auth-mount-ledger-and-docs
    • Worktree: ../objectstack-10534(per-repo;⛔ 不在共享主检出上编辑;⛔ 不用 git stash)
    • File surface: packages/plugins/plugin-auth/src/auth-route-ledger.ts + content/docs/** 的相关页面。⛔ 零 packages/spec 所有权;⛔ 不碰 content/docs/releases/(卡面自己已把它排除在语料之外,别再碰回来)。
    • Container & model: claude-opus-5
    • Clause-②: no —— 补文档与补账本行,不改任何路由的挂载、行为或接受/拒绝集。⚠️ 若你发现某个 mount 的行为与账本将要声明的不符,⛔ 不要改行为去迁就账本,停下报告。

    本轮范围

    ✅ 建议 1 — 5 个仍未文档化的 mount:/admin/sso/request-domain-verification、/admin/sso/verify-domain、/admin/unban-user、/bootstrap-status、/set-initial-password。
    ✅ 建议 2 — 9 个两个账本半边都不在的 mount,补为 source: 'objectstack' 行。

    ⛔ 建议 3(sdk bridge / REGISTRAR_FILE_RE)不做 —— 卡面明写 scripts/docs-audit/affected-docs.mjs 被开着的 PR #10501 占用,补救要等它落地。先核实 #10501 是否仍开着(自己查,别采信我或卡面的转述);仍开则连碰都不要碰那个文件。
    ⛔ 建议 4(加门禁)不做 —— 那是门禁设计决策,不是本卡的机械活。若你在实现中拿到了「这条门禁该怎么写」的具体证据,写进报告,由我决定是否立卡。

    判断成分:不要机械填表

    ⚠️ 这两条都不是纯机械,卡面自己留了口子:

    • 建议 1 允许「或为每个记录一个刻意的 server-only, not user-facing 裁决」,并且点名 bootstrap-status 很可能就是这一类。文档化和裁决为不面向用户是两种不同的答案,逐个给理由。
    • 建议 2 的每一行都要定 disposition(client-bound 还是 server-only)。这决定它是否进入那 55 行 client-bound 群体,不是格式字段。add-member 之所以第二次隐形,正是因为它是 server-only 且无 client。

    ⛔ 任何一个你定不下来的,停下并在报告里点名,不要为了让表填满而猜一个 disposition。今晚本车道已经两次因为「选最近的允许选项」而差点写下假声明。

    实现要求

    1. 两条腿都要有正向对照 —— 卡面自己做了(leg 1 用 organization/add-member 已知在集内、leg 2 用 /config 与 /organization/add-member 各返回 1)。你重新测量时照做:零结果在有对照之前不是发现。
    2. 路由枚举以卡面的表达式为准(rawApp.(\${basePath}/…`),排除 rawApp.allcatch-all 与.well-known/*),但**自己跑一遍**确认仍是 17 条 —— main 从 4544be2` 起已经动过。
    3. 文档页里必须出现字面 wire path。卡面记了一个陷阱:set-initial-password 在 authentication.mdx 里被提到过名字但没有 URL,add-member 在 tenancy-modes.mdx 里被提了三次也没有 URL —— 「被提到」不等于「被文档化」,这正是它们隐形的方式。
    4. 门禁并集在最终提交之后、干净工作树上跑。⚠️ 今晚四个门禁未被派生点名(class [finding] Every PM dispatch list is short by the same ~5 changeset-triggered gate families — they are path-derivable, but the changeset does not exist yet when the list is derived #10309),其中两个专咬 plugin-auth:显式加跑 pnpm check:route-envelope 与 check:dispatcher-error-vocabulary(各含 --self-test),不要等派生点名。另外你改 content/docs/**,显式加跑 pnpm --filter @objectstack/spec check:docs 与 check:affected-docs。
    5. ⚠️ @objectstack/plugin-auth 的 TEST_DEBT:卡 [finding] @objectstack/plugin-auth's TEST_DEBT records 109 where tsc now reports 98 — an 11-error surplus opened by PR #10614 #10615 报告账本记 109 而 tsc 现报 98。⛔ 不要动那个数字,也不要跑 --lower;若你的改动让它变化,如实报告。
    6. 消融:对新增的账本行,预测签名先写再跑(删掉一行应让门禁点名它),git hash-object 证明恢复逐字节一致。

    Generated by Claude Code

  5. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 10534,
      "status": "done",
      "branch": "claude/issue-10534-auth-mount-ledger-and-docs",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10660",
      "premise_still_valid": true,
      "summary": "Follow-ups 1+2 only. Re-ran both census legs myself on 47aff0938 with positive controls: population still 17. Leg 2 reproduced the card exactly (9 unledgered). Leg 1 did NOT: the card's test is a substring test and /admin/sso/register is a PREFIX of /admin/sso/register-saml, so it was credited as documented on the strength of its longer sibling's URL. Boundary-aware, it is 6 undocumented, not 5. All 6 are now documented with literal wire paths (none earned the 'server-only, not user-facing' verdict -- bootstrap-status specifically does NOT, because client.auth.bootstrapStatus() is a shipped SDK method ledgered 'sdk', which falsifies the card's hypothesis). 8 of the 9 unledgered mounts gain source:'objectstack' rows, all disposition:'server-only', decided per route by measuring who builds the URL rather than defaulted. The 9th, POST /api/v1/auth/set-initial-password, is DELIBERATELY NOT LEDGERED and escalated below -- see open_questions. The conformance pin over the objectstack-sourced set moves 3 -> 11 in the same edit. No route's mounting, behaviour or accept/reject set changed (Clause-2 no); zero packages/spec and zero content/docs/releases/** ownership.",
      "tests": "All at final commit 3115f7939, clean worktree; every exit code captured BEFORE any pipe (cmd > file 2>&1; EXIT=$?), never after a tail. Gate family derived by `node scripts/pm/dispatch-gates.mjs` with NO path arguments (5 paths, 20 matched families + convention-triggered set). SUITE: `pnpm --filter @objectstack/plugin-auth test` -> 'Test Files 61 passed (61) / Tests 1359 passed (1359)'. TYPES: `pnpm --filter @objectstack/plugin-auth typecheck` (tsc --noEmit) exit 0. GATES, each quoted as it printed: check:affected-docs '✓ affected-docs self-test: 281 cases pass.'; check:docs-audit-scope '✓ docs-accuracy-audit scope is in sync with content/docs/: 189 hand-written doc(s).' + '✓ release-owned pages are in scope and read-only: 9 page(s) ... review-only'; check:doc-anchors '✅ ... slug parity, custom ids, duplicate counters ... (277 live fragment links)'; spec check:docs '✅ 229 generated files in sync with packages/spec'; check:nul-bytes 'OK (scanned 6212 text file(s) ... no raw ASCII control bytes).'; check:route-envelope + --self-test '✓ Plugin-mounted Hono routes -- 12 module(s) audited ... 8 conformant, 0 ratcheted'; check:dispatcher-error-vocabulary + --self-test '8 shapes + 102 assertions OK'; check:where-matcher '✓ ... 271 matcher(s) discovered, 271 answer the combinator battery correctly'; check:cross-package-test-inputs 'OK: 13 package(s) read outside themselves, all declared'; check:type-check-coverage '✓ --self-test -- 23 semantic + 24 observation + 25 re-measure + 28 built-closure + 9 auto-lowering case(s) hold.'; plus exit-0 on check:engine-double-contract, check:query-options-erasure, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:role-word, check:docs-redirects, check:published-readme-links, check:objectui-changeset, check:changeset-gate-self-tests, check-changeset-no-major, check-empty-changeset, check-adr-0087-registration, and spec check:{empty-state,liveness,strictness-ledger,variant-docs}. CLASS #10309 CONFIRMED LIVE: the derivation named NEITHER check:route-envelope NOR check:dispatcher-error-vocabulary for this change set -- both were run only because the dispatch named them explicitly. DECLARED NARROWING: check:type-check-debt --re-measure was NOT run (it refuses on an unbuilt worktree and needs the full workspace closure built under the container-shared lock); substituted the package typecheck above; CI runs the ratchet regardless. TEST_DEBT (#10615): untouched -- number not edited, --lower not run, nothing re-measured; the edited test file is pre-existing so no package gains/loses test-file coverage. ABLATION, signature predicted in writing BEFORE running: deleting the 'POST /api/v1/auth/admin/unlock-user' row should make the objectstack-mounted pin fail with own at 10 instead of 11, NAMING the route. Observed exactly: 'AssertionError: expected [ ...(10) ] to deeply equal [ ...(11) ]' with '- \"POST /api/v1/auth/admin/unlock-user\",', 'Tests 1 failed | 9 passed (10)'. NO REBUILD IS INVOLVED and that is PROVEN, not asserted: the suite imports the ledger relatively (from './auth-route-ledger', conformance test line 27), so resolution cannot go through the package exports field and no dist/ sits between edit and run. Mutation confirmed on disk (row grepped to 0), not from an editor exit code. Restore proven byte-identical: git hash-object 123e1a87dcedf63dc188cf680c2fc3ed08417913 before and after, git diff --quiet exit 0. CENSUS CONTROLS: leg 1 -- '/api/v1/auth/sign-in/email strict-present: true', '/api/v1/auth/zzz-not-a-route present: false', 'add-member strict-present: true' (the card's own add-member control now correctly reports false since #10050's PR landed, so it was replaced rather than dropped); leg 2 -- ledger grep returns 1 for /config and 1 for /organization/add-member; SDK-caller grep control -- bootstrap-status 2, sign-in/email 2, get-session 6, so the zeros for all 8 ledgered routes are real zeros.",
      "open_questions": [
        {
          "question": "What disposition should `POST /api/v1/auth/set-initial-password` carry? I stopped rather than guess. Evidence: zero ObjectStackClient methods build it (control-verified). Its only real caller is @object-ui/auth's createAuthClient, which builds exactly four auth URLs -- /config, /get-session, /list-accounts, /set-initial-password -- and the first THREE are all ledgered `sdk` with ObjectStackClient methods (auth.getConfig, auth.me, auth.accounts.list). Its own sibling branch in the same Console password card, changePassword, is also ledgered `sdk`. So its peer group is uniformly SDK-expressed except itself, which is the shape of `gap` ('should be in the SDK and is not'), not `server-only` ('deliberately not SDK surface'). But `gap` is ratcheted to <= 0 by auth-route-ledger.conformance.test.ts. Contrast /admin/import-users, which is ALSO called by a hand-written objectui client and which I DID ledger server-only: its peer group (/admin/create-user, /admin/ban-user, /admin/set-user-password) is uniformly SDK-absent, so the intent claim is consistent there. The discriminator is the peer group, not convenience -- which is exactly why this one cannot be settled from the tree.",
          "options": [
            "A. Rule it genuinely server-only by decision -- the Account/Console SPA owns its own auth client and ObjectStackClient deliberately does not cover the SSO-onboarding password path. Costs nothing mechanically; I can add the row in a follow-up. Requires a maintainer to actually MEAN it, because the row then declares intent.",
            "B. Rule it a real `gap` -- accept that ObjectStackClient should express setInitialPassword. Needs the gap ratchet moved from 0 to 1 with a note, and the row says so honestly until the SDK method lands.",
            "C. Close the gap instead: add `auth.setInitialPassword` to ObjectStackClient and ledger the row `sdk`. This is a public-surface widening and a behaviour change, so it is out of Clause-2 `no` and belongs on its own card.",
            "D. Leave it unledgered (today's shipped state). Rejected as a resting place: unaccounted-for is precisely the state this card exists to end, and it would silently survive as the one remaining member of the set."
          ],
          "recommendation": "B, then C as the follow-through. B is the only option that states today's truth without either a false intent claim or a surface change: the SDK does not express it, its entire peer group does, and nobody has decided that asymmetry is intentional. A is the option I would have taken if I were filling the table, and I think it would be a false declaration -- there is no evidence of deliberateness, only of absence. Moving a ratchet from 0 to 1 is visible, reviewable and reversible; writing 'deliberately not SDK surface' about a route nobody deliberated is neither. If the maintainer prefers not to move the ratchet at all, A is defensible ONLY as an explicit ruling recorded in the row's note (the add-member precedent does exactly that, with a quoted decision), never as a default."
        },
        {
          "question": "Follow-up 3's stated blocker has cleared, and the PM asked me to verify rather than take it on trust. PR #10501 (which the card and the claim comment both describe as OPEN and owning scripts/docs-audit/affected-docs.mjs) is MERGED -- closed 2026-08-21T02:46:52Z, merged true, commit 4e7229a6. I did not touch that file, because the scope ruling admits follow-ups 1 and 2 only. But the reason for the fence no longer holds.",
          "options": [
            "A. File follow-up 3 (teach REGISTRAR_FILE_RE about auth-plugin.ts, or declare registrars explicitly) as its own card now -- the card's own text says 'file it as its own card after #10501 lands', and it has landed.",
            "B. Hold it until this PR merges, so the bridge work starts from the completed ledger rather than a moving one."
          ],
          "recommendation": "A. The two do not overlap in file surface (this PR touches no scripts/), and the card's own instruction was conditional on #10501 landing, which it has. Worth noting for whoever takes it: the auth ledger's client-bound row count is unchanged by this PR -- all 8 new rows are server-only with no `client`, so the '55 of 55 unreachable' figure the card measured does not move, and the bridge card's baseline is still valid."
        }
      ],
      "out_of_scope_findings": [
        "filed as #10658: check-error-code-casing prints '✓ no lowercase error codes in 4346 scanned file(s) (ADR-0112)' while two ObjectStack-authored lowercase codes ship in packages/plugins/plugin-auth/src/register-sso-provider.ts (lines 411 request_domain_verification_failed, 465 verify_domain_failed) -- a literal reached through an `||` fallback (`code: parsed?.code || 'x'`) matches none of the gate's four recognizers, all of which require the quote to sit directly after the position token. Verified with a positive control on the gate's own regex: it returns ['oops_bad'] for the direct spelling and [] for the fallback spelling. Repo-wide extent measured at exactly 2 (non-test). Same 'partial read reported as complete' class PR #10501 just closed for a different scanner, and the third instance of the pattern across the error-code gates after #9223 and #9460. NOT fixed here: different defect class from this card, and renaming the codes is wire-visible (a dogfood test pins verify_domain_failed by name).",
        "NOT filed, reported for the PM's follow-up-4 decision as instructed -- concrete evidence on how the proposed mount-without-ledger-row gate should be written. (1) A mount-vs-docs leg MUST match the wire path with a right boundary; matching by substring silently credits a prefix route to its longer sibling, which is the artifact that made this card's own leg 1 read 5 instead of 6. (2) The mount-vs-ledger leg is cheap and already nearly exists: auth-route-ledger.conformance.test.ts's 'the objectstack-mounted rows' pin asserts the source:'objectstack' set exactly, so it already fails when a row DISAPPEARS. What it cannot do is fail when a MOUNT appears with no row, because both sides of it are hand-written -- the missing half is enumerating rawApp mounts from auth-plugin.ts source and diffing against the ledger, which this card proves is mechanically reliable (the same expression reproduced 17/17 across two different commits). (3) The gate must exclude rawApp.all and rawApp.use, or the catch-all and the IP-gate middleware both read as unledgered mounts. (4) A disposition cannot be inferred mechanically -- the set-initial-password question above is exactly the judgement such a gate would force onto whoever adds route 18, which is an argument FOR the gate, but it means the gate's failure text should demand a disposition with a rationale, not merely a row."
      ]
    }

    Generated by Claude Code

  6. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    PM 验收:ACCEPT —— PR #10660(8/9 入账 + 6 篇文档),第 9 条已升级维护者

    Clause-②: no,实现未证伪。按内容核对:无 packages/spec/**,无 content/docs/releases/**。

    ⭐ 两处推翻了卡面,都是重新测量抓出来的

    ① leg 1 的数字错了:是 6 个未文档化,不是 5 个。 卡面用子串判定,而 /admin/sso/register 是 /admin/sso/register-saml 的前缀 —— 它靠兄弟路由的 URL 被记成"已文档化"。加右边界后原形毕露。

    ⚠️ 这不只是个数字修正。它和卡面自己记录的另一个陷阱(set-initial-password 被提到名字但没有 URL、add-member 在 tenancy-modes.mdx 被提了三次也没 URL)是同一类:匹配得太松,把「看起来被讨论过」当成了「被文档化」。一次是人读松了,一次是正则读松了。

    ② bootstrap-status 不是 server-only。 卡面推测它"很可能是",实测 client.auth.bootstrapStatus() 是已发货的 SDK 方法且已 ledger 为 sdk。假设被数据否掉。

    第 9 条:POST /api/v1/auth/set-initial-password —— 停下上报,判断正确

    开发没有填这一行,理由我完全认可,而且它给出的判别器是对的:

    • 零个 ObjectStackClient 方法构建它(对照验证过)
    • 唯一真实调用者 @object-ui/auth 的 createAuthClient 构建四个 auth URL:/config、/get-session、/list-accounts、/set-initial-password —— 前三个全部 ledger 为 sdk 且各有 ObjectStackClient 方法
    • 同一张 Console 密码卡里的兄弟分支 changePassword 也是 sdk

    ⇒ 它的同侪组一致地由 SDK 表达,唯独它不是 —— 这是 gap(「本该在 SDK 里而不在」)的形状,不是 server-only(「刻意不做 SDK 面」)。但 gap 被 conformance 测试棘轮到 <= 0。

    对照组做得尤其好:/admin/import-users 同样被手写 objectui client 调用,它被入账为 server-only 了 —— 因为它的同侪组(/admin/create-user、/admin/ban-user、/admin/set-user-password)一致地 SDK 缺席,意图声明在那里自洽。

    判别器是同侪组,不是方便性。

    开发对自己动机的检查值得原样保留:

    A is the option I would have taken if I were filling the table, and I think it would be a false declaration — there is no evidence of deliberateness, only of absence.

    ⇒ 已升级维护者(移动 gap 棘轮 0→1 属于放宽棘轮,不是 PM 可自决的)。

    围栏已解除:#10501 已合并

    开发按我的要求自己核实,而不是采信我或卡面的转述:PR #10501 已于 2026-08-21T02:46:52Z 合并(commit 4e7229a6)。卡面和我的 claim 都说它「开着」—— 我们都过时了。

    ⇒ follow-up 3(sdk bridge / REGISTRAR_FILE_RE)的阻塞理由已不存在,卡面自己的措辞是「#10501 落地后单独立卡」。开发没有越界去做(范围裁定只含 1+2),只是报告。一条给接手者的事实:本 PR 新增的 8 行全是 server-only 无 client,所以「55 of 55 unreachable」不动,bridge 卡的基线仍有效。

    其余核对

    class #10309 第五次确认

    派生既没点名 check:route-envelope 也没点名 check:dispatcher-error-vocabulary —— 两个都只因为我在派发指令里明写才跑了。⚠️ 这已经是我连续第三次靠手写补进去才躲过一次红。


    Generated by Claude Code

  7. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    Ruling — set-initial-password disposition: option C

    #10660's dev refused to pick a disposition for the ninth unledgered mount and escalated it, correctly: server-only and gap were the only two moves the ledger's vocabulary offered, and both are false in different ways. The maintainer has ruled C — bind the route into the SDK, then ledger it as sdk.

    Re-measured before ruling, on origin/main at 81845c65a:

    AUTH_ROUTE_LEDGER disposition distribution
      sdk 54 · server-only 1 · disabled 1 · gap 0
    
    packages/client/src
      setInitialPassword     0        getConfig       1
      set-initial-password   0        auth.me         1
                                      accounts        10
                                      changePassword  1
    

    Both spellings zero while four siblings in the same corpus are non-zero, so the absence is real and not a broken expression.

    Why not A (server-only)

    The dev's own peer-group test already answered this and the re-measurement agrees: the route's three sibling URLs in the same createAuthClient are all ledgered sdk with ObjectStackClient methods, and its sibling branch in the same Console card (changePassword) is sdk too. server-only is a claim about intent, and the intent it would claim is contradicted by the route's own neighbourhood. That is the "state machine lying" failure named in #10240's triage — a table that closes because a row was written to make it close.

    The dev put this plainly when declining to fill it in: A is the one I would pick if I went to fill in the form, and I believe it would be a false statement. That judgement is upheld.

    Why not B (gap)

    gap describes the situation accurately. It is still refused, and the reason is not "the suite would go red" — it is which suite and in which direction:

    packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts
    :189  it('gap and mismatch counts only shrink', () => {
    :192  expect(AUTH_ROUTE_LEDGER.filter((e) => e.disposition === 'gap').length).toBeLessThanOrEqual(0);
    

    The test is named for its own direction. Filing this row as gap would not be recording a number — it would be reversing a ratchet, which is the one thing a ratchet exists to refuse. That is the honest mirror image of the const-hoist move this repo ruled out by name on 2026-08-21: there, a counter was made to read green with no wire change; here, a counter would be made to read red so a row could be parked under it. Both substitute the instrument for the fact.

    Why C, and what it costs

    C is the only option that leaves every reading true: the route is client-reachable (measured), the SDK does not express it (measured), so the SDK gains the method and the ledger records what is then true. sdk is 54 of 56 rows — it is the ledger's rule, not one of several equals.

    Its cost is real and is why it is not folded into #10660: it widens published SDK surface, which is a Clause-② contract-review decision and not a dispatching seat's to land.

    Filed as two cards, in a fixed order:

    The ordering is mechanical, not stylistic. auth-route-ledger.conformance.test.ts:152 requires every sdk row to name a client method; landing the row first would put client: 'auth.setInitialPassword' in the ledger while no such method exists — a claim of coverage the SDK does not have, which is verbatim what packages/client/src/route-ledger-coverage.test.ts was written to catch (its header cites #3528: the ledger equivalent of the day would have said resume → automation.resume while no such method existed). The reverse order is safe only because the dispatcher ledger's * /auth/** prefix row absorbs the URL in the interim — which client-url-conformance.test.ts:123-129 itself labels weak evidence, so #10975 must report the matched pattern by name before and after.

    Correction on this card's own tracking — and a gate blind spot

    #10660's body opened with Fixes #10534. That is wrong: #10660 delivers follow-ups 1 and 2 only, and this card's remaining scope is follow-up 3 (the REGISTRAR_FILE_RE bridge), follow-up 4 (the mount-vs-ledger gate — which is what this card's own title claims is missing), and now the ledger row that #10975 carries. On merge the card would have closed silently with that scope open.

    Changed to Part of #10534. Verified from GitHub's derived state rather than from re-reading the body: before the edit this card reported closed_by_pull_requests: total_count 1 naming #10660; after, total_count 0, references []. #10660's head commit is unchanged (3115f7939) and its body is otherwise byte-for-byte what it was — the load-bearing content (both ablation hashes, the four commit SHAs, the gate verdict table, the six section headings) was read back and compared.

    ⚠️ No gate would have caught this. scripts/check-partof-closing-keyword.mjs fires on a body that declares Part of #N and Fixes #N for the same card; its own self-test at :246 asserts the opposite case is clean —

    t('an ordinary closing PR with no Part-of declaration is clean', verdict('Fixes #8476').exit, EXIT_CLEAN);
    

    A partial-scope PR that says only Fixes is indistinguishable, to that gate, from a complete one. Its blocking rationale at :197 — the card closes SILENTLY on merge — describes exactly the state it cannot see here. Recorded as a finding rather than fixed on this card; it is a different defect class from anything in this sweep.

    Remaining scope on this card

    1. Follow-up 1 — document the undocumented mounts — docs(plugin-auth): ledger and document the unaccounted ObjectStack auth mounts #10660 (6 of 6, boundary-aware).
    2. Follow-up 2 — ledger the unledgered mounts: 8 of 9 in docs(plugin-auth): ledger and document the unaccounted ObjectStack auth mounts #10660; the ninth is ledger POST /api/v1/auth/set-initial-password as an sdk objectstack mount #10975.
    3. Follow-up 3 — whether the sdk bridge should reach the auth surface. Unblocked — docs(plugin-auth): ledger and document the unaccounted ObjectStack auth mounts #10660 verified fix(docs-audit): a PARTIAL ledger read is a verdict, not a smaller number #10501 merged 2026-08-21T02:46:52Z, so the stated fence has cleared.
    4. Follow-up 4 — the mount-with-no-ledger-row gate. Not built. docs(plugin-auth): ledger and document the unaccounted ObjectStack auth mounts #10660's report contributes one design constraint worth carrying into it: the wire-path match needs a right boundary, or a prefix route is silently credited to its longer sibling — the artifact that made this card's own leg 1 read 5 when it was 6.

    This card stays open.


    Generated by Claude Code

  8. os-project-manager commented on Aug 22, 2026

    @os-project-manager
    Collaborator

    H8 配对写入:pm:dispatched → pm:queue

    交付 PR #10660 已于 2026-08-22T04:12:10Z 合并,本卡仍挂 pm:dispatched。

    按 services 席在 5377191597 里列的剩余范围定级,不新增判断:

    1. follow-up 1 文档化未文档的 mount —— docs(plugin-auth): ledger and document the unaccounted ObjectStack auth mounts #10660 已交付(6 of 6,带右边界)。
    2. follow-up 2 ledger 未入账的 mount —— 9 之中 8 已入,第九行是 ledger POST /api/v1/auth/set-initial-password as an sdk objectstack mount #10975。
    3. follow-up 3 sdk 桥是否触达 auth 面 —— 已解封(fix(docs-audit): a PARTIAL ledger read is a verdict, not a smaller number #10501 于 2026-08-21T02:46:52Z 合并,所述围栏已清)。
    4. follow-up 4 「有 mount 无 ledger 行」的门禁 —— 未建,而这正是本卡标题声称缺的东西。

    ⇒ 剩余物可派发、无阻塞方,故 pm:queue。

    ⚠️ 顺带记:#10660 正文原写 Fixes #10534(会在合并时静默关掉带着上面 3 项的卡),已由该席改为 Part of #10534 并用 closed_by_pull_requests 从 1 → 0 验证 —— 那次是靠人读出来的,没有门禁会抓。

    本笔是维护者指令下的 H8 清账(2026-08-22,session session_014kugUSM5M5fJBsk1f8KdtN):只做合并的配对写入,⛔ 未改卡面范围、未动 assignee。


    Generated by Claude Code

  9. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 10534,
      "status": "done",
      "branch": "claude/issue-10534-mount-ledger-gate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11149",
      "premise_still_valid": true,
      "summary": "Follow-up 4 ONLY. Built `scripts/check-auth-mount-ledger.mjs` (`pnpm check:auth-mount-ledger`, wired into lint.yml with no paths filter): it enumerates rawApp mounts from auth-plugin.ts SOURCE and diffs them against BOTH halves of auth-route-ledger.ts, which is the half the hand-written conformance pin structurally cannot supply. (a) POPULATION RE-MEASURED on current main bbe643c08: STILL 17 -- 12 accounted for by a reviewed AUTH_ROUTE_LEDGER row, 4 shadowing a BETTER_AUTH_MOUNTED_SURFACE path, 1 pending. Third reproduction across a third commit. (b) Gate GREEN on main (exit 0); RED on a REAL added mount, not a regex assertion. (c) Right boundary pinned directly, on the real tree AND in --self-test, both directions. (d) The failure text demands a disposition WITH a rationale and is backed mechanically: an objectstack row missing `client:` (sdk) or a substantive `note:` (server-only/disabled/public) FAILS, so pasting a row does not satisfy it. (e) The ninth mount is NOT required and does NOT redden: POST /api/v1/auth/set-initial-password sits in a declared, issue-bearing, shrink-only PENDING_DISPOSITION entry naming #10975, printed on every clean run. Clause-2 `no` holds and was not falsified: no runtime surface, no route's mounting/behaviour/accept-reject set changed. The only packages/** edit is COMMENT-ONLY, in the conformance pin whose text still told readers this gate did not exist. Zero packages/spec, zero content/docs/releases/**, and scripts/docs-audit/affected-docs.mjs untouched. NOT touched: follow-up 3 and the #10975 row.",
      "tests": "All at final commit b86bd9e76, clean worktree; every exit code captured BEFORE any pipe (cmd > file 2>&1; EXIT=$?), never after a tail. GATE UNION derived by `node scripts/pm/dispatch-gates.mjs` with NO path arguments (5 paths, 27 families), RE-DERIVED after the final commit -- 27 both times, no drift. ALL EXIT 0: check:auth-mount-ledger, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:entry-guard, check:node-version, check:objectui-changeset, check:parse-guard, check:pnpm-filter-targets, check:required-contexts, check:shard-attestation, check:slot-lookup, check:test-source-alias, check:type-check-coverage, check:type-source-resolution, check:workflow-status-functions, check-adr-0087-registration, check-aggregator-roster, check-changeset-no-major, check-ci-filter-parity, check-empty-changeset, check-plugin-teardown-shape, check-required-contexts, check-shard-attestation, check-step-collectors, docs-audit/check-affected-docs, plus the convention-triggered test-file set (check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:type-check-coverage, check:type-check-debt). ADDED BY HAND per class #10309, both exit 0: `pnpm check:route-envelope` (each runs --self-test first) -> '✓ Express-style response modules -- 4 module(s) discovered and audited ... 2 conformant, 2 ratcheted' (both ratchets pre-existing #9559, tick-down only); `pnpm check:dispatcher-error-vocabulary` -> 'check-dispatcher-error-vocabulary: OK — 21 unregistered code-stamping site(s), all classified'. Also added by hand: check:nul-bytes -> 'OK (scanned 6454 text file(s) ... no raw ASCII control bytes).' CLASS #10309 CONFIRMED A SIXTH TIME: the derivation named NEITHER route-envelope NOR dispatcher-error-vocabulary for THIS change set. PARTIAL CLOSURE, verified not assumed: the derivation DOES name check:auth-mount-ledger for a diff touching auth-plugin.ts or auth-route-ledger.ts (ran dispatch-gates against each path; both print 'matched via ... gate source'), because the two input paths are module-scope string literals, which is what extractWatchHints reads. UNREACHABLE BY CONSTRUCTION, recorded and NOT claimed as coverage: check:driver-memory-census and check-test-completeness (their declared populations name no tracked path). Note the dispatch named check:optional-error-sink here; the live derivation on this tree did not place it in the unreachable bucket -- reporting what the derivation printed, not the recollection. RATCHET RAN TO A REAL VERDICT, no declared narrowing: check:type-check-debt initially REFUSED ('--re-measure cannot run: 30 workspace dependenc(ies) ... have no built type entry point') -- recorded as NOT MEASURED, then the full closure was built (`turbo run build --filter=./packages/* --filter=./packages/*/*`, 70/70 successful) and it re-ran to 'check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 246.1s, 1895 raw tsc error(s) total, none above its recorded number.' TEST_DEBT (#10615) UNTOUCHED: number not edited, --lower not run. The ratchet reports '@objectstack/plugin-auth: TEST_DEBT records 109, tsc now reports 97 (-12) -- the entry can be lowered. Not an error' -- pre-existing surplus, left as found. Refining the dispatch's figure: 109 vs 97, not 109 vs 98. THE TEST-FILE TSCONFIG TRAP WAS CHECKED, NOT ASSUMED: packages/plugins/plugin-auth/tsconfig.json excludes **/*.test.ts, so the package typecheck cannot see the only packages/** file this PR touches. Ran an explicit test-inclusive program on BOTH sides: 97 errors with my file, 97 with origin/main's file -- identical -- and `git diff origin/main` on that file contains ZERO non-comment changed lines. `pnpm --filter @objectstack/plugin-auth typecheck` exit 0 with the script name echoed and two real tsc invocations (not a zero-match; an earlier run exit 2 on examples/basic-usage.ts was the unbuilt-SELF artifact of `^...`, fixed by building the package, not declared away). SUITE: `pnpm --filter @objectstack/plugin-auth test` -> 'Test Files 69 passed (69) / Tests 1449 passed (1449)'. An earlier run showed 51 failed files, which was the documented unbuilt-closure trap ('Failed to resolve entry for package @objectstack/objectql' x19); `pnpm --filter '@objectstack/plugin-auth^...' build` fixed it -- and note `--concurrency=2` after a pnpm --filter is FORWARDED to tsup and fails the build, the flag-placement trap, hit and corrected. ABLATIONS, all five signatures predicted IN WRITING before any mutation, all on the REAL tree, every one matched: (A) added a real rawApp.post for /admin/zzz-ablation-probe -> predicted RED with 1 unaccounted-mount and scope 18; observed exit 1, exactly one '[unaccounted-mount] POST /api/v1/auth/admin/zzz-ablation-probe', 'scope: 18 rawApp mount(s)'. (B) added a real rawApp.all AND rawApp.use under basePath -> predicted GREEN, 17 mounts, lanes 2->4; observed exit 0, '17 ObjectStack auth mount(s)', '4 lane(s)'. (C) deleted the real 'POST /api/v1/auth/admin/sso/register' row leaving register-saml -> predicted RED naming it WITH the prefix block; observed exit 1 and 'NOT CREDITED, DELIBERATELY: `POST /api/v1/auth/admin/sso/register` is a strict PREFIX of `POST /api/v1/auth/admin/sso/register-saml`'. (D) added a real ledger row for set-initial-password (simulating #10975) -> predicted RED resolved-pending; observed exit 1, 'now HAS a ledger row (#10975 landed). Delete the entry -- that is how the ratchet comes down.' (E) mutated reconcile to credit by substring -> predicted --self-test RED on the boundary assertions; observed exit 1 naming 'a strict-prefix mount was CREDITED to its longer sibling -- the #10534 defect, reintroduced'. ⚠️ E ALSO MEASURED SOMETHING WORTH CARRYING: under substring accounting the PRODUCTION run stays GREEN (exit 0) on a clean tree -- only --self-test catches it. The right-boundary defect is invisible to the gate's own real run, which is why the repo's --self-test-first convention is load-bearing and why the 38 green assertions are not vacuous. RESTORES proven byte-identical by git hash-object and re-run to a real verdict: auth-plugin.ts 56c783b059d81e058c540079db786c204bc8351f, auth-route-ledger.ts 123e1a87dcedf63dc188cf680c2fc3ed08417913, check-auth-mount-ledger.mjs ca32bb02b8d5e134ea281ea98b0dceee43751386, conformance test d4c9cb0dfffc25cbef248afa78195c60c2b1647b; `git diff --quiet` exit 0; then --self-test 38/38 and the gate green again. NO REBUILD IS INVOLVED AND IT IS ARGUED FROM THE FILES, FALSIFIABLY: the gate readFileSync's two .ts paths under packages/plugins/plugin-auth/src/ -- no import of the package, no exports resolution, no dist/; the single occurrence of the string 'dist' in the script is the header sentence saying so. Demonstrated in BOTH states: green when packages/plugins/plugin-auth/dist did not exist AT ALL, and green again after the full workspace closure was built. A REFUSAL FIRED DURING DEVELOPMENT AND IS WORTH RECORDING: the first array parser looked for '];' at column 0, and both arrays in auth-route-ledger.ts close as ',];' on the last row's own line -- the gate exited 2 with 'NOT MEASURED', rather than reporting an empty population as a clean tree. Fixed to bracket-matching with string literals skipped; the refusal is why it was caught in the first minute instead of shipping as a green no-op.",
      "open_questions": [
        {
          "question": "Follow-up 3's fence: I verified it myself rather than taking the dispatch's word. PR #10501 is MERGED -- merged_at 2026-08-21T02:46:52Z, merged true, head commit 4e7229a6, labels include skip-changeset. So the stated blocker on the REGISTRAR_FILE_RE / sdk-bridge work has cleared. I did not touch scripts/docs-audit/affected-docs.mjs (my diff is 5 files, none under scripts/docs-audit). One fact for whoever takes follow-up 3: this PR does not move its baseline either -- the gate adds no ledger rows at all, so the '55 of 55 client-bound rows unreachable' figure is untouched, and `docs-audit/check-affected-docs` ran green here.",
          "options": [
            "A. File follow-up 3 as its own card now -- it is a design fork with two named options (teach REGISTRAR_FILE_RE about auth-plugin.ts, or declare registrars explicitly rather than by filename convention), which is exactly why it does not belong on a dispatching seat.",
            "B. Hold it until this PR merges."
          ],
          "recommendation": "A. The two do not overlap in file surface and this PR adds no ledger rows, so there is no moving baseline to wait for. Worth carrying into that card: #10501's own PR body records that the recognizer is deliberately NOT widened and that its --self-test pins the narrow population so a silent widening reds -- so whoever changes REGISTRAR_FILE_RE inherits a gate that is already built to notice the change, in both directions."
        },
        {
          "question": "PENDING_DISPOSITION is an exemption list, and exemption lists rot. I made it shrink-only (PENDING_MAX = 1), reconciled in BOTH directions (an entry whose mount is gone fails; an entry whose route has since been ledgered fails; an entry naming no issue fails), and printed on every clean run so an open question cannot become a quiet green. That is the strongest shape I could give it without a maintainer decision, but the ratchet ceiling is a number a future seat could raise, and raising it is the one move that would turn this gate into a parking space -- the same failure the ledger's own `disabled` docblock warns about.",
          "options": [
            "A. Leave PENDING_MAX at 1 as written, with the script saying raising it is a maintainer decision and never the co-equal option (today's state).",
            "B. Additionally pin PENDING_MAX in a test outside the script, so raising it cannot be done in the same edit that needs it raised.",
            "C. Drop PENDING_DISPOSITION entirely once #10975 lands, so the mechanism has no residue to be reused."
          ],
          "recommendation": "A now, and C as the follow-through once #10975 lands -- but only if the entry is genuinely the last one. I did NOT pick B on my own: it is real hardening, but it puts a second hand-written number in a second file, which is the shape that produced this card's original problem (two hand-written sides that can drift). If the maintainer wants B, it belongs on its own card with that trade stated, not folded in here."
        }
      ],
      "out_of_scope_findings": [
        "NONE FILED, and that is a measured result rather than a skipped step. The gate found no unaccounted mount beyond the one already carded (#10975), so it produced no new defect to file. I searched open issues before concluding: the two candidates I considered are both already recorded and neither is mine to re-file -- (1) `scripts/check-partof-closing-keyword.mjs` cannot see a partial-scope PR that declares only `Fixes`, which the maintainer recorded on this card on 2026-08-22 as a finding deliberately not fixed there; (2) `@objectstack/plugin-auth` TEST_DEBT's 12-error surplus, which is #10615 / #6376 and which I left untouched.",
        "REPORTED, NOT FILED -- one fact about this repo's gate conventions that the ablation measured and that I did not see written down anywhere: for a gate whose defect class is a MATCHING RULE (substring vs. right boundary), the production run on a clean tree cannot detect its own regression -- ablation E left `check:auth-mount-ledger` at exit 0 while --self-test went red. That makes `--self-test` the only instrument for this class, and it means a future seat who 'simplifies' a gate's self-test to save CI time removes the only thing watching the rule. It is stated in the new gate's header, but it may generalise to check:route-envelope and the other matching-rule gates. Left for the PM to decide whether it is worth a card of its own; I did not file it because it is an observation about a convention, not a defect with a landing surface."
      ]
    }

    Generated by Claude Code

  10. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    PM review of PR #11149 (follow-up 4 only). Accepted.

    ⭐ I verified the gate by RUNNING it, not by reading the diff

    First time today I could do that, and it is only possible because of how the gate was built: it readFileSyncs two .ts paths, imports nothing from the workspace (only two sibling scripts/*.mjs), and contains the string dist exactly once — in the header sentence saying it does not use one. So I reconstructed a minimal tree in a scratch directory from origin/main's own two source files and ran it with bare node, touching no checkout:

    leg result
    origin/main, unmodified exit 0 — 17 ObjectStack auth mount(s), all accounted for (12 by a reviewed ledger row, 4 shadowing a vendor-declared path, 1 pending)
    one real extra rawApp mount with no ledger row exit 1 — [unaccounted-mount] GET /api/v1/auth/admin/pm-probe-unledgered, scope: 18 rawApp mount(s)
    restored, re-run to a real verdict exit 0, back to 17

    That is the load-bearing pair, reproduced independently of the seat's own harness. The PENDING line prints on every clean run as claimed, so the open question cannot decay into a quiet green.

    ⭐ Worth naming as a design property rather than an accident: a gate that any reviewer can run with bare node is a gate whose green can be independently audited. The no-workspace-import shape bought that.

    The population is now measured three times, across three commits

    17, again, on bbe643c08. #10534's census said the expression reproduced 17/17 across two commits; this is the third. The enumeration is mechanically reliable, which is what licenses building a gate on top of it.

    ⚠️ Ablation E is the finding I want carried beyond this PR

    Under substring accounting the production run stays green (exit 0) on a clean tree — only --self-test reddens. The right-boundary defect is invisible to the gate's own real run.

    That generalises: for a gate whose defect class is a matching rule, the production run cannot detect its own regression. --self-test is then the only instrument watching the rule, and a future seat who "simplifies" a self-test to save CI time removes the only thing watching it. The seat stated this in the gate's header and flagged that it may generalise to check:route-envelope and the other matching-rule gates.

    I am treating that as worth its own card, not as an observation — it is a standing hazard across a whole family of gates. Filing separately rather than riding this PR.

    The four measured constraints, each honoured and each observable

    1. Right boundary — exact equality on METHOD /full/wire/path, and when an unaccounted mount stands in a prefix relation the failure text says so (NOT CREDITED, DELIBERATELY: … is a strict PREFIX of …). The property is in the output, not implicit in a comparison operator.
    2. rawApp.all / rawApp.use excluded — counted and reported as lanes, so the exclusion is visible rather than silent. Ablation B confirmed adding both leaves mounts at 17 and lanes at 4.
    3. A row alone does not satisfy it — an objectstack row must carry the evidence its disposition claims (client: for sdk, a substantive note: otherwise). ⭐ The seat is honest about the limit: the note floor is a floor, not a proof, and what it buys mechanically is that adding three words is no longer the cheapest path. That is the right claim to make; "this prevents false dispositions" would have been overclaiming.
    4. A partial read is never a complete one — unrecognised verb, non-literal path, unresolved interpolation are each a finding; a missing file or zero-yield parse exits 2 with NOT MEASURED.

    ⭐ Constraint 4 earned its keep during development, and this is the part I would keep if I kept one sentence: the first array parser looked for ]; at column 0, while both arrays close as ,]; on the last row's own line — the gate refused (exit 2) rather than reporting an empty population as a clean tree. A gate that reported zero mounts as "all accounted for" would have shipped as a permanent green no-op. It was caught in the first minute because refusing was built in from the start.

    The ninth mount is carried honestly

    POST /api/v1/auth/set-initial-password sits in a declared, issue-bearing, shrink-only PENDING_DISPOSITION entry naming #10975, reconciled in both directions — an entry whose mount is gone fails, an entry whose route has since been ledgered fails (ablation D), an entry naming no issue fails. It does not demand a row that does not exist, so nothing here front-runs #10975/#10974.

    Instrument discipline

    The test-file tsconfig trap was checked, not assumed: plugin-auth/tsconfig.json excludes **/*.test.ts, so the package typecheck cannot see the only packages/** file this PR touches. A test-inclusive program was run on both sides — 97 with this branch, 97 with origin/main, identical — and the diff of that file has zero non-comment changed lines. That is the fifth time today this exclusion has hidden something from a locally-green check.

    check:type-check-debt first refused (--re-measure cannot run: 30 workspace dependenc(ies) … no built type entry point) — recorded as NOT MEASURED, closure built 70/70, re-run to a real verdict. TEST_DEBT (#10615) untouched; the seat also refined my figure: the surplus is 109 vs 97, not 109 vs 98.

    An earlier suite run showed 51 failed files — the documented unbuilt-closure trap — and the seat also hit and corrected the flag-placement trap (--concurrency=2 after a pnpm --filter is forwarded to tsup and fails the build). Both reported rather than smoothed over.

    Class #10309 — sixth confirmation, and a partial closure

    The derivation named neither check:route-envelope nor check:dispatcher-error-vocabulary for this change set; both ran only because the dispatch spelled them out, and both are green.

    ⭐ But the seat then measured something better than a complaint: the derivation does name check:auth-mount-ledger for a diff touching auth-plugin.ts or auth-route-ledger.ts — verified by running dispatch-gates against each path — because the two input paths are module-scope string literals, which is what extractWatchHints reads. So the new gate is derivable from the day it lands rather than depending on someone remembering it. That is a small, concrete narrowing of the class.

    It also declined to repeat my recollection: my dispatch listed check:optional-error-sink as unreachable-by-construction; the live derivation on this tree did not place it there, and the seat reported what the derivation printed rather than what I told it. Correct.

    ⛔ My own slip on this card, corrected

    I dispatched this seat without swapping pm:queue → pm:dispatched — I went straight to the dispatch and never wrote the label. The card sat in pm:queue for the whole run, which reads to any other agent as an open invitation on a card already being worked. That is the exact failure the claim protocol exists to prevent, and it was mine. Swapped now.

    Scope

    Part of #10534, verified — the body opens with it and states explicitly that no closing keyword is present. That matters here because check-partof-closing-keyword.mjs only fires when a body declares both Part of and Fixes; a partial-scope PR saying only Fixes is invisible to it, which is how #10660 nearly closed this card silently.

    Untouched as required: scripts/docs-audit/affected-docs.mjs (follow-up 3), #10975/#10974, packages/spec, content/docs/releases/**. The only packages/** edit is comment-only.

    Remaining on this card after this PR: follow-up 3 (the REGISTRAR_FILE_RE bridge — a two-option design fork, and the seat verified the fence has cleared: #10501 merged 2026-08-21T02:46:52Z) and the #10975 ledger row.

    CI convergence is mine to read; I am subscribed and will verify by job name.


    Generated by Claude Code

  11. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    os-dev-report

    Follow-up report — supersedes the gate-union section of 5382661078. CI was red at b86bd9e76 on check:ratchet-remedy-authority; fixed and pushed. New head 47ced12bf.

    {
      "issue": 10534,
      "status": "done",
      "branch": "claude/issue-10534-mount-ledger-gate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11149",
      "premise_still_valid": true,
      "summary": "Follow-up 4 only, unchanged in substance: the gate is green on main with the population still 17, red on a real added mount, and the right boundary pinned in both directions. This report covers the CI red at b86bd9e76 and its fix at 47ced12bf. THE RED WAS OURS AND IT WAS NOT PAPERWORK. check:ratchet-remedy-authority (#8435) reported TWO findings locally, not the one relayed to me: an UNCLASSIFIED (missing control entry) and, underneath it, a real UNMARKED VIOLATION. The gate was telling an author who could not decide a disposition to add their own route to PENDING_DISPOSITION -- a shrink-only exemption list -- as a co-equal option. That is the parking-space move this card already ruled against once, offered by my own gate. CLASSIFICATION, decided against the convention's vocabulary rather than by picking the quietest word: 'marked'. The main population's remedy (ledger the mount, with evidence) targets AUTH_ROUTE_LEDGER, which GROWS as routes are added and is not a ratchet at all -- the detector agrees, anchoring only on PENDING_DISPOSITION -- so there is NO split the vocabulary fails to express, and I am not reporting a false finding to that effect: the convention classifies only the ratchet-expanding path. Both PENDING_DISPOSITION paths (adding a route; raising PENDING_MAX) expand a shrink-only exemption list and neither is the landing author's. 'refused' is the stronger shape and would be FALSE here -- the list holds a legitimate entry, granted by the maintainer ruling of 2026-08-22 on this card. There IS a real act, with a real owner; naming the owner is honest, denying the act is not. check-skills-token-ratchet.mjs (#10473) records exactly this reading for exactly this shape (a new gate, two remedy paths, marked because 'there IS a legitimate act, with an owner'); I reached it independently and then checked it against that precedent. Both author-facing messages now carry the token, four new self-test pins hold its PLACEMENT, and CONTROL records the gate as 'marked' with the split written into its why. Scope respected: affected-docs.mjs untouched, no #10975 row, PENDING_MAX not raised, TEST_DEBT untouched.",
      "tests": "New head 47ced12bf, clean worktree, exit codes captured BEFORE any pipe. THE FIX, OBSERVED NOT ASSUMED: check:ratchet-remedy-authority --list moved 'unmarked (1)' -> 'unmarked (0)' and 'marked (7)' -> 'marked (8)'. Gate verdict: 'OK  check-ratchet-remedy-authority: 122 scripts swept (scripts/*.{mjs,mts}); 8 mark the expanding remedy MAINTAINER-ONLY, 4 turn it down outright, 110 hand out no ratchet-expanding remedy. Control corpus: 21 hand-classified scripts, set-equality audited both ways.' Its --self-test also exit 0. ⚠️ ONE INTERMEDIATE MEASUREMENT WORTH RECORDING: my first attempt wrote the token as the escape '\\u26D4' out of byte-discipline reflex, and the sweep STAYED unmarked -- U+26D4 is a printable symbol, not a control byte, and this convention is a TEXT sweep, so the literal character is required and is what every other instrumented gate uses. Caught by re-observing the verdict instead of assuming the edit worked. RE-DERIVED UNION on 47ced12bf: `node scripts/pm/dispatch-gates.mjs` with NO paths -> 6 paths, 28 families. THE SECOND COMMIT CHANGED THE UNION: 27 -> 28, the one added family being check:ratchet-remedy-authority itself. ALL 30 RUN, ALL EXIT 0: check:ratchet-remedy-authority, check:auth-mount-ledger, check:route-envelope, check:dispatcher-error-vocabulary, check:nul-bytes, check:entry-guard, check:parse-guard, check:changeset-gate-self-tests, check:objectui-changeset, check:node-version, check:pnpm-filter-targets, check:required-contexts, check:shard-attestation, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:workflow-status-functions, check:cross-package-test-inputs, check-adr-0087-registration, check-aggregator-roster, check-changeset-no-major, check-ci-filter-parity, check-empty-changeset, check-plugin-teardown-shape, check-step-collectors, docs-audit/check-affected-docs, check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:type-check-coverage, plus check:type-check-debt. Hand-added again per class #10309 (route-envelope, dispatcher-error-vocabulary, nul-bytes) -- all exit 0. VERDICT LINES: 'check-auth-mount-ledger --self-test: 42 assertions OK'; 'check-auth-mount-ledger: OK -- 17 ObjectStack auth mount(s), all accounted for (12 by a reviewed ledger row, 4 shadowing a vendor-declared path, 1 pending a disposition)'; 'check-nul-bytes: OK (scanned 6454 text file(s) ... no raw ASCII control bytes)'; 'check-type-check-coverage --re-measure: OK -- 33 ledger entr(ies) re-measured in 247.3s, 1895 raw tsc error(s) total, none above its recorded number' (run on a FULLY REBUILT closure, 70/70 tasks, under the shared lock -- a real verdict, no declared narrowing); plugin-auth 'Test Files 69 passed (69) / Tests 1449 passed (1449)' and typecheck exit 0. The inter-commit delta is `scripts/` ONLY (git diff b86bd9e76 47ced12bf --stat: 2 files), so no ledgered package's tsc inputs moved -- but the ratchet was rebuilt and re-run on the new head anyway rather than argued by inference. NEW ABLATIONS, predicted in writing first, both matched: (F) strip the token from the DEMAND BLOCK only -> predicted my --self-test RED on 1 pin and the farm sweep GREEN; observed exactly that. (G) strip it from BOTH author-facing messages, leaving the const -> predicted my --self-test RED on 2 pins and the farm sweep STILL GREEN; observed exactly that, sweep exit 0 still counting this gate among the 8 marked. ⚠️ G IS THE LOAD-BEARING ONE: the cheapest fix that turns CI green -- add the const and the CONTROL row, touch no message -- SATISFIES THE FARM SWEEP WHILE DECLARING SOMETHING UNTRUE, because that sweep checks token PRESENCE and delegates placement to per-gate self-tests ('Presence here, placement there', its own header). Only the four new placement pins catch it. This is by design and documented, not a defect in that gate. Restores byte-identical: check-auth-mount-ledger.mjs 496075b88cb1b4d116d7574d93280afc16bcd14a, git diff --quiet exit 0, then --self-test 42/42 and both gates green. ⚠️ ONE FALSE RED CAUGHT AND CORRECTED RATHER THAN REPORTED: a first pass of the union on the new head showed five gates red with ERR_MODULE_NOT_FOUND -- I had recreated the worktree (after the earlier cleanup) without pnpm install. Reinstalled, re-ran, all green. Recorded because a red read from an uninstalled tree is exactly the kind of number that gets filed as a finding. PR BODY UPDATED to the new head; 'Part of #10534' kept and re-verified from GitHub's DERIVED state after the edit -- closed_by_pull_requests total_count 0, references [] -- not by re-reading the body.",
      "open_questions": [
        {
          "question": "The coordinator asked whether the #8435 vocabulary can express my gate's split (author may fix the main population freely / the exemption paths need leave), and said to report a finding if it cannot. My answer is that it CAN, and I am declining to file a finding I do not believe -- which is the same discipline this card applied to set-initial-password. The convention only ever classifies the RATCHET-EXPANDING remedy; the main population's remedy targets AUTH_ROUTE_LEDGER, which is not a ratchet, so it is out of the convention's scope by construction rather than inexpressible in it. The detector confirmed this independently by anchoring only on PENDING_DISPOSITION. And the registry already carries a gate with two differently-owned remedy paths (check-skills-token-ratchet.mjs, #10473), recording the split in the entry's `why` field -- which is where I put mine.",
          "options": [
            "A. Accept the reading above: one verdict on the ratchet-expanding path, the split recorded in `why`. No finding filed.",
            "B. Treat the single-verdict-per-file shape as a real limitation and file it, so a gate with two owners can declare two verdicts."
          ],
          "recommendation": "A. B would be a finding manufactured to satisfy an instruction rather than because the tree is wrong: a per-path verdict would add a second hand-written declaration per gate, and the existing `why` field already carries the reasoning where a reviewer reads it. If the maintainer disagrees, the precedent to revisit is #10473's entry, not mine -- it made this choice first and this card only followed it."
        },
        {
          "question": "PENDING_DISPOSITION's shape is now stronger than when I first reported it -- both paths that expand it name their owner, and four self-test pins hold that placement. My earlier open question (whether to pin PENDING_MAX outside the script) is partly answered: the #8435 token now makes raising it a declared maintainer act rather than a comment nobody reads. What remains open is only whether that is enough.",
          "options": [
            "A. Leave it as it now stands: shrink-only, both expanding paths marked MAINTAINER-ONLY, placement pinned per-gate.",
            "B. Additionally pin PENDING_MAX from outside the script.",
            "C. Delete PENDING_DISPOSITION entirely once #10975 lands."
          ],
          "recommendation": "A now, C once #10975 lands and the entry is genuinely the last. I still decline to pick B unprompted, for the reason I gave before: it adds a second hand-written number in a second file, which is the drift shape this card exists to close."
        }
      ],
      "out_of_scope_findings": [
        "filed as #11155 (unassigned, label `finding`): check:ratchet-remedy-authority sweeps scripts/*.{mjs,mts} -- EVERY gate script in the repo -- and dispatch-gates can derive it for NONE of them. Measured on 47ced12bf: `dispatch-gates scripts/check-auth-mount-ledger.mjs | grep -c ratchet-remedy-authority` -> 0, and 0 again for a hypothetical new gate path, with a positive control confirming the expression works (check:entry-guard and check:cross-package-test-inputs ARE named for both paths, via their declared 'scripts/**'). --residue puts it in the UNDETERMINED bucket -- 'their sources name no path at all' -- because its population is built by readdirSync and its only filename literals are bare CONTROL basenames, which extractWatchHints refuses as too generic for lacking a path separator. It appears in my union now ONLY because this PR edits the sweep's own file. The remedy is the one dispatch-gates' own residue note names (declare the subtree spelling); I flagged for whoever takes it that whether hintCovers accepts a brace glob is unverified and must be measured, not assumed. NOT fixed here: different defect class, and its landing surface is another gate.",
        "⚠️ CLASS #10309, SEVENTH CONFIRMATION ON THIS CARD -- AND THE FIRST THAT COST A RED. The six earlier confirmations were all covered by a gate the dispatch named by hand; this one was not named by the derivation OR by hand, so it ran for the first time in CI. That is the case the class is actually about, and it is materially worse than the covered ones: a hand-named gate turns a derivation miss into a no-op, while an unnamed one turns it into a CI round. The two are worth separating in the class's own record -- #11155 is the concrete cause for this instance, but the general lesson is that 'the dispatch named it' has been silently doing the derivation's job, and its coverage is whatever the dispatching seat happened to remember.",
        "REPORTED, NOT FILED (carried forward from the first report, now with a second instance): for a gate whose defect class is a MATCHING or PLACEMENT rule, the production run on a clean tree cannot detect its own regression. Ablation E left check:auth-mount-ledger at exit 0 while its --self-test went red; ablation G left check:ratchet-remedy-authority at exit 0 while my per-gate pins went red. Two independent instances now, in gates written years apart, and in both the self-test is the ONLY instrument. A future seat who trims a gate's --self-test to save CI time removes the only thing watching the rule. Still an observation about a convention rather than a defect with a landing surface, so still not filed -- but the second instance makes it worth the PM's judgement."
      ]
    }

    Generated by Claude Code

  12. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    PM review — round 2 rework on PR #11149 (b86bd9e76 → 47ced12bf). Accepted.

    Type Check · source gates, the exact lane that failed, is green on the new head; zero failures across the run.

    ⭐ The red was not paperwork, and I relayed it wrong

    I sent this back describing a missing declaration. Locally the gate reported two findings, not the one CI surfaced — the UNCLASSIFIED entry, and underneath it a real unmarked violation:

    The gate was telling an author who could not decide a disposition to add their own route to PENDING_DISPOSITION — a shrink-only exemption list — as a co-equal option.

    That is the parking-space move this card already ruled against once, and it was being offered by the gate built to prevent it. The set-initial-password ruling on this card rejected gap precisely because filing it there would have been reversing a ratchet so a row could be parked under it; the new gate's own author-facing text was recommending the same move to the next author.

    A gate that hands out the evasion it exists to catch is worse than no gate — it launders the move as sanctioned. That is now fixed in both author-facing messages, with four self-test pins holding the token's placement.

    ⛔ The seat declined an instruction of mine, and was right

    I wrote: if the convention's vocabulary cannot express the split, that is a finding — report it. The seat's answer:

    My answer is that it CAN, and I am declining to file a finding I do not believe — which is the same discipline this card applied to set-initial-password.

    The reasoning holds: the #8435 convention only ever classifies the ratchet-expanding remedy. The main population's remedy targets AUTH_ROUTE_LEDGER, which grows as routes are added and is not a ratchet at all — so it is outside the convention by construction, not inexpressible within it. The detector agreed independently, anchoring only on PENDING_DISPOSITION. And the registry already carries a gate with two differently-owned remedy paths (check-skills-token-ratchet.mjs, #10473) recording the split in the entry's why — reached independently, then checked against that precedent.

    ⭐ It also rejected refused as the stronger-sounding option because it would be false: the pending list holds a legitimate entry granted by this card's own 2026-08-22 ruling. "There IS a real act, with a real owner; naming the owner is honest, denying the act is not."

    That is the second time today a seat has correctly refused an instruction I gave. I would rather record that than let it pass as compliance — an instruction that invites a manufactured finding is a defect in the instruction.

    ⚠️ Ablation G is the one to carry

    The cheapest fix that turns CI green — add the const and the CONTROL row, touch no author-facing message — satisfies the farm sweep while declaring something untrue. That sweep checks token presence and delegates placement to per-gate self-tests ("Presence here, placement there", its own header). Only the four new placement pins catch it.

    By design, not a defect in that gate. But it means the honest fix and the cheap fix are indistinguishable to CI, and only a per-gate test separates them. This is now the second measured instance of the pattern I filed as #11150 — after ablation E on the matching rule. Two independent instances, in gates written years apart, and in both the --self-test is the only instrument. I have added this to #11150 as its second data point.

    Two instrument errors caught by re-observing rather than assuming

    • The token was first written as the escape ⛔ out of byte-discipline reflex, and the sweep stayed unmarked — U+26D4 is a printable symbol, not a control byte, and this convention is a text sweep. Caught by re-reading the verdict instead of trusting that the edit worked.
    • A first union pass on the new head showed five gates red with ERR_MODULE_NOT_FOUND — a recreated worktree without pnpm install. Reinstalled, all green. Reported rather than filed, with the right reason: "a red read from an uninstalled tree is exactly the kind of number that gets filed as a finding."

    Class #10309 — the seat sharpened my framing, and filed the cause

    Seventh confirmation on this card, and the first that cost a red. The seat separated the two cases better than I had:

    a hand-named gate turns a derivation miss into a no-op, while an unnamed one turns it into a CI round … "the dispatch named it" has been silently doing the derivation's job, and its coverage is whatever the dispatching seat happened to remember.

    That is the real finding, and it lands on me: my hand-written gate lists have been masking the derivation's gaps for six rounds, and their coverage is my memory. This round I did not know to name check:ratchet-remedy-authority, so nothing covered it.

    Concrete cause filed as #11155: check:ratchet-remedy-authority sweeps scripts/*.{mjs,mts} — every gate script in the repo — and dispatch-gates can derive it for none of them. Measured with a positive control (check:entry-guard and check:cross-package-test-inputs ARE named for the same paths). Its population is built by readdirSync and its only filename literals are bare CONTROL basenames, which extractWatchHints refuses for lacking a path separator. It appears in this PR's union only because this PR edits the sweep's own file. The seat also flagged that whether hintCovers accepts a brace glob is unverified and must be measured — not assumed.

    Verification on the new head

    Union re-derived on 47ced12bf: 27 → 28 families, the added one being check:ratchet-remedy-authority itself — a second commit changing the union, for the second time today. All 30 run, all exit 0, including the three added by hand again. check:auth-mount-ledger --self-test moved 38 → 42 assertions; the gate still reports 17 mounts all accounted for. check:ratchet-remedy-authority --list moved unmarked (1) → unmarked (0), marked (7) → marked (8).

    check:type-check-debt --re-measure run on a fully rebuilt closure (70/70) to a real verdict — and note the seat rebuilt and re-ran it even though the inter-commit delta is scripts/ only, rather than arguing by inference that no ledgered package's tsc inputs moved. Restores byte-identical; Part of #10534 re-verified from GitHub's derived state (closed_by_pull_requests total_count 0) rather than by re-reading the body.

    Scope held: affected-docs.mjs untouched, no #10975 row, PENDING_MAX not raised, TEST_DEBT untouched.

    Once the remaining jobs are terminal and green by job name, I flip ready and arm — Clause-② is no and this card carries no needs:contract-review.


    Generated by Claude Code

  13. changed the title [-]finding: `add-member` was not the only one — 6 of 17 ObjectStack auth mounts are undocumented and 9 of 17 are in no ledger, and nothing catches a mount that lands without either[/-] [+]auth-mount coverage — follow-ups 1/2/4 LANDED; remaining scope is follow-up 3 (sdk bridge) + the #10975 ledger row (body below is the original census, now historical)[/+] on Aug 23, 2026
  14. removed their assignment
    on Aug 23, 2026
  15. claude commented on Sep 4, 2026

    @claude
    Contributor

    Figure restatement: the sdk bridge's reach moved — 45 → 61, in two steps, and this card is the second

    Posted under maintainer ruling A on #11857 (comment 5537060359, 2026-09-04 decision batch #31), which made restating the published figure a condition of the change. This issue quotes the reachable-rows figure, so it needs to know when it moved and why.

    The figure this issue cites was 45 of 221 (measured on 9ff11921a). It is now 61 of 219, and the move is two separate things — neither of them a recognizer regression:

    step reading why
    45 of 221 → 47 of 219 tree drift, no mechanism change measured across #12966; rest-server.ts unrolled a loop into a literal path: (one new tail, selecting on two ledgers), and three already-unreachable rows were deleted from rest-route-ledger.ts. Recorded in scripts/docs-audit/README.md.
    47 of 219 → 61 of 219 this card — the evidence route lands route sources go 12 → 17: the 12 registration call sites, plus 5 packages/spec API contract declarations now admitted as a second kind of route source. Tails 44 → 78.

    Control, re-derived first (the card's own rule: if the control does not reproduce, the delta means nothing). On 460134af8 the unmodified tool reads 12 route sources / 44 tails, against the card's 12 / 43. The one-tail difference is tree content, not mechanism: running today's tool against the card's own ref a6eca9223 reproduces 12 / 43 / 45 exactly, so nothing in PR #12146's ceiling work moved the discovery reading.

    What actually changed, per ledger (--bridge-coverage on 460134af8, before → after):

    ledger before after
    storage 7 of 7 unreachable 0 of 7 — closed
    i18n 3 of 3 1 of 3
    rest 42 of 84 40 of 84
    runtime 64 of 69 61 of 69
    plugin-auth 56 of 56 56 of 56 — unchanged; declared upstream and catch-all-mounted, so no discovery change reaches it
    discovery-gap rows 14 0

    discovery-gap → 0 is the load-bearing reading: every row the ceiling said a widened discovery could reach is now actually reached, so what remains unreachable is unreachable for reasons no discovery change touches.

    ⚠️ Note the card's own 45 → 59 is not what landed, and should not be quoted from here on. Both endpoints moved with the tree, and the guard admits 5 files rather than the card's 8 — the fixture and the benchmark it counted were already excluded by isTestFile (#12965), and the connector-action input in packages/spec/src/conversions/registry.ts is kept out by a runtime-registration signal rather than admitted. The figure of record is 47 → 61 on 460134af8, re-measured rather than copied.

    No number in this issue's own analysis is invalidated — the reach it describes grew; it did not turn out to have been wrong.

    Implemented in PR #15274.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions