Repository navigation
Description
Found while implementing #10974 (binding auth.setInitialPassword). Not a live wire defect today — every sdk row in AUTH_ROUTE_LEDGER happens to name a method that does exist. What is missing is the check that keeps it that way.
The claim that does not hold
packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts:152 is the auth ledger's "every sdk entry names its client method" test. Read literally, it checks only that the field is non-empty:
const sdkWithout = AUTH_ROUTE_LEDGER.filter((e) => e.disposition === 'sdk' && !e.client).map((e) => e.route);!e.client is falsy-on-absent. A row spelled client: 'auth.setInitialPasword' (typo), or naming a method that was later renamed or deleted, passes this test unchanged.
The resolution check exists for every other ledger, and not this one
The sibling ledgers each have a client-side half that instantiates an ObjectStackClient and asserts typeof resolve(e.client) === 'function':
| ledger | resolution guard |
|---|---|
ROUTE_LEDGER (dispatcher) |
packages/client/src/route-ledger-coverage.test.ts |
REST_ROUTE_LEDGER |
packages/client/src/rest-route-ledger-coverage.test.ts |
STORAGE_ROUTE_LEDGER |
packages/client/src/service-route-ledger-coverage.test.ts |
I18N_ROUTE_LEDGER |
same file |
DATASOURCE_ROUTE_LEDGER |
same file |
AUTH_ROUTE_LEDGER |
none |
Measured — every consumer of the symbol repo-wide:
packages/qa/dogfood/test/route-ledger-live-mount-parity.dogfood.test.ts
packages/client/src/client-url-conformance.test.ts → uses r.route only (compiled into PATTERNS)
packages/client/src/route-ledger-response-schema.test.ts → uses rows for response-shape, not client resolution
packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts
scripts/check-ratchet-remedy-authority.mjs · scripts/check-auth-mount-ledger.mjs
Nothing in that list resolves an auth row's client string against a client instance. client-url-conformance.test.ts imports AUTH_ROUTE_LEDGER but reads r.route exclusively — it matches URLs the SDK builds against routes, which is the opposite direction and does not read the client field at all.
Why it matters
AUTH_ROUTE_LEDGER is the largest of the ledgers (56 rows, 54 of them sdk) and it is the one whose rows are written by hand alongside a vendor surface that moves under it. The failure this guard family was built for is stated in route-ledger-coverage.test.ts's own header:
This is the direction #3528 shipped through: the ledger equivalent of the day would have said "resume → automation.resume" while no such method existed.
That exact shape is currently unguarded for auth.
Note for whoever picks this up
#10974's body asserts that a sdk row naming a nonexistent method is "the exact failure packages/client/src/route-ledger-coverage.test.ts was written for". That test reads ROUTE_LEDGER (the dispatcher's), not AUTH_ROUTE_LEDGER, so it would not have caught an auth row. The sequencing argument in #10974 partly rests on that — worth reading together with it.
Likely shape of the fix
Add an AUTH_ROUTE_LEDGER leg to packages/client/src/service-route-ledger-coverage.test.ts (or a sibling file), reusing the brokenIn helper already there. The ledger is pure data with no imports, so the existing relative-source-import convention applies unchanged — no new package edge.
Refs
Found during #10974 · #3528 (the originating failure) · #3563 / #3636 (the guard family) · #3656 (the auth ledger's charter)
Activity
claude commentedon Aug 23, 2026
Triage: lands in
packages/plugins/plugin-auth(conformance test tightening); rationale: the ledger test claims "every sdk entry names its client method" but only checks non-emptiness — restoring a declared invariant, no product fork.pm:queue,domain:services, Bug. Serial note: #10974 (in-flight, same auth surface) is where this was found — answer fold-or-serial against its file surface at dispatch.Generated by Claude Code
claude commentedon Aug 23, 2026
os-dev-report
{ "issue": 11359, "status": "done", "branch": "claude/issue-11359-auth-ledger-client-resolution", "pr": "https://github.com/objectstack-ai/objectstack/pull/11403", "premise_still_valid": true, "summary": "FOLD-OR-SERIAL: SERIAL. #10974's PR #11360 changes exactly two files (packages/client/src/index.ts + .changeset/client-auth-set-initial-password.md) — disjoint from this diff, so this proceeded independently. PLACEMENT: followed the CARD (packages/client), not the triage note (plugin-auth), on measurement: all 3 repo-wide files carrying a `typeof resolve(...) === 'function'` ledger guard live in packages/client/src and 0 live elsewhere; @objectstack/plugin-auth has no dependency on @objectstack/client in any of dependencies/devDependencies/peerDependencies, so the guard there would have created a backwards plugin-auth->client package edge; and the auth ledger's own header already says 'the client-side guard imports it as a relative SOURCE file'. Chose a sibling file, packages/client/src/auth-route-ledger-coverage.test.ts, over a fourth leg in service-route-ledger-coverage.test.ts (the card allowed either): that file's title and docblock are a statement about the #3636 SERVICE tranche ('all THREE server surfaces') and auth is a plugin, so folding it in would have made the file's own title false. Two legs: every client-naming row resolves to a function, plus a guard-the-guard leg (population floor + positive and negative controls derived from the ledger) that makes this card's ablation permanent. Also added a comment at the server-side test (auth-route-ledger.conformance.test.ts) recording that it checks PRESENCE only and naming where resolution now happens — the misleading title is what let this sit unnoticed. ZONE-2 ASSUMPTIONS: (1) resolved above in the card's favour; (2) confirmed and STRONGER than assumed — no new package edge AND no new cross-package radius, because packages/plugins/plugin-auth/src/auth-route-ledger.ts was already declared in CROSS_PACKAGE_TEST_INPUTS['@objectstack/client'] and already in turbo.json's @objectstack/client#test inputs (client-url-conformance.test.ts and route-ledger-response-schema.test.ts already read it the same way); (3) confirmed — 55 rows carry a `client` name (54 sdk + one `disabled` row that deliberately keeps its name, #7735 auth.deleteUser) and ALL 55 resolve. No live defect, so clause-② `no` still holds and needed no re-grade. ⚠️ TWO THINGS FOR YOU, NOT FIXED BY ME: (a) #10974's ordering argument cites 'the exact failure packages/client/src/route-ledger-coverage.test.ts was written for' — that test reads ROUTE_LEDGER, not AUTH_ROUTE_LEDGER, so it would not have caught an auth row and nothing would have until this PR. The argument's CONCLUSION survives and is strengthened (with no guard at all a premature auth row would have sat silent), but the authority cited for it is false. PR #11360 is still open/draft, so nothing has merged on it. (b) #10975's body is stale in a way that will mislead whoever picks it up: it says the `source: 'objectstack'` pin holds 'exactly three routes' and that the card takes it 'from 3 to 4' — that list grew to ELEVEN in #10534, so the PR-body argument #10975 demands is written against a number that no longer exists. DELIBERATELY NOT ADDED: a reverse-direction leg ('every client.auth.* method is backed by a ledger row', which the service file carries for storage and i18n). It would go red the moment #11360 merges and stay red until #10975 lands, inverting the maintainer-ruled option-C ordering — a separate decision, not a rider on a guard-hole card. No changeset: `skip-changeset`, label applied via the additive POST endpoint and READ BACK present alongside the size-labeler's own writes (size/m, tests, skip-changeset).", "tests": "All measurements taken at final commit 2de8c3ef71 (clean tree, `git rev-parse --short HEAD`), in worktree /home/user/objectstack-11359. Heavy steps serialized through scripts/pm/os-verify-lock.sh; verdicts read from each gate's own printed line with the exit code captured before any pipe (never a bare $? after a tail). BUILD FIRST: os-verify-lock -c \"pnpm --workspace-concurrency=2 --filter '@objectstack/client^...' build\" -> 'BUILD_EXIT=0' and 'os-verify-lock: VERDICT command-exit 0 · held the lock 375s · waited 240s'. SUITES: `pnpm --filter @objectstack/client test -- --maxWorkers=2` -> 'Test Files 24 passed (24) / Tests 316 passed (316)' (pnpm echoed '> @objectstack/client@17.2.0 test', so this was not a zero-match silent green). `pnpm --filter @objectstack/client typecheck` -> EXIT=0, and its check:test-typecheck leg printed 'OK — @objectstack/client's test layer compiles under packages/client/tsconfig.test.json; 0 file(s) / 0 error(s) held in test-typecheck-debt.json', so the new test file adds no debt. `pnpm --filter @objectstack/plugin-auth typecheck` -> EXIT=0. ABLATION (signature predicted in writing BEFORE running: typo auth.changePassword -> auth.changePasword; new leg red naming that row, old server-side check green). REBUILD QUESTION, ANSWERED: the mutation subject packages/plugins/plugin-auth/src/auth-route-ledger.ts is imported by the suite as a RELATIVE SOURCE path — no `exports` resolution and no vitest alias, so dist is not on its resolution path and no rebuild is needed for the mutation to reach the test; corroborated by check-test-source-alias's KNOWN_UNALIASED_TEST_IMPORTS, where @objectstack/client's dist-resolved imports are six package specifiers and @objectstack/plugin-auth is not among them. The dependency closure was built first anyway (above), so nothing ran against a stale dist. MUTATION CONFIRMED ON DISK by anchored grep in BOTH directions plus a control, not by the editor's exit code: removed text \"client: 'auth.changePassword'\" count = 0 (expect 0); injected text \"client: 'auth.changePasword'\" count = 1 (expect 1); untouched sibling row \"client: 'auth.changeEmail'\" count = 1 (expect 1); blob 123e1a87dced -> 2390fa5efc84. RESULTS: LEG0 baseline LEG0_EXIT=0 'Tests 2 passed (2)'. LEG1 new guard under mutation LEG1_EXIT=1, 'AssertionError: auth ledger entries claiming a client method that does not exist: POST /api/v1/auth/change-password -> client.auth.changePasword: expected [ Array(1) ] to deeply equal []', 'Tests 1 failed | 1 passed (2)' — exactly the predicted signature. LEG2, the point of the card: the EXISTING server-side check (plugin-auth auth-route-ledger.conformance.test.ts -t 'names its client method') under the SAME mutation LEG2_EXIT=0, 'Tests 1 passed | 9 skipped (10)' — green, waving the typo through. RESTORE LEG: restored blob 123e1a87dced equals the original, 'RESTORE=BYTE-IDENTICAL' via git hash-object, removed text back (count 1) and injected text gone (count 0); LEG3 re-run after restore LEG3_EXIT=0 'Tests 2 passed (2)'. Whole script ran under trap restore EXIT INT TERM. SECOND REVERSE CONTROL (on the one declaration this PR adds; predicted red before running): removing the new glob from CROSS_PACKAGE_TEST_INPUTS and its $TURBO_ROOT$ twin from turbo.json made check-cross-package-test-inputs exit 1 with 'FAIL: cross-package test inputs are not declared consistently. - @objectstack/client names path(s) no declared glob covers ... packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts (named in packages/client/src/auth-route-ledger-coverage.test.ts)'; both files restored and verified IDENTICAL against their HEAD blobs. So the declaration is load-bearing, not decorative. GATE UNION derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, NO hand-supplied paths, and RE-DERIVED after the final commit (same 4-path change set, same 12 families; stderr confirmed the answer came from objectstack-ai/objectstack at 9337cd8104 and that --repo held against the checkout's origin). All green: check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, node scripts/check-ci-filter-parity.mjs ('OK: all 89 declared cross-package glob(s) (77 unique) are covered by `core` or `crosspkg` ...'), node scripts/check-plugin-teardown-shape.mjs ('✓ ... 63 Plugin implementation(s) across 4536 source(s)'), node scripts/docs-audit/check-affected-docs.mjs; plus the convention-triggered families for a new test file: check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:type-check-coverage — all EXIT=0. NOTE ON TWO OF THEM: `pnpm check:ci-filter-parity` and `pnpm check:plugin-teardown-shape` exited 254 (ERR_PNPM_NO_SCRIPT — those two have no package.json script alias); re-run as `node scripts/...mjs` they are the greens quoted above. DECLARED NARROWINGS (2, both stated rather than skipped): (1) `pnpm check:type-check-debt --re-measure` needs the whole workspace built and was NOT run locally; it cannot move here — plugin-auth's tsconfig.json excludes **/*.test.ts, its ledger entry is the hidden test layer (errors: 109), and this PR's only change to that package is comment lines inside an existing `it` body (no identifiers, no imports). The structural half, check:type-check-coverage, is green. (2) `pnpm lint` (eslint . --no-inline-config, repo-wide) narrowed to the changed files, with all three pieces of evidence: population read from eslint's OWN config (it reports turbo.json as 'File ignored because no matching configuration was supplied', so 3 of the 4 paths are in scope); counts read from --format json (4 entries, 0 errors, 1 warning and that warning is the turbo.json ignore notice, IDENTICAL on origin/main's turbo.json via a base-version control run); and configuration invariance for untouched files — eslint.config.mjs states in its own words that this repo 'runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, test or not', so no untouched file's verdict can move because of this diff. CI runs both in full regardless. CI STATUS AT REPORT TIME: in_progress — reported at draft-PR time per the standing clause, not after CI convergence.", "open_questions": [ { "question": "#10974's ordering argument cites `packages/client/src/route-ledger-coverage.test.ts` as the guard that would catch a `sdk` row naming a nonexistent method. That is false — it reads ROUTE_LEDGER, not AUTH_ROUTE_LEDGER. Its PR #11360 is open and draft (needs:contract-review, pm:blocking), so nothing has merged on the false authority. What, if anything, should be corrected?", "options": [ "A — leave both as-is: the conclusion (SDK method first, then the ledger row) is unchanged and in fact strengthened by this card, since before PR #11403 NOTHING guarded an auth row; the cited authority is decoration on a sound argument", "B — edit #10974's body to cite the real guard (now `packages/client/src/auth-route-ledger-coverage.test.ts`, which this PR adds) so a contract reviewer does not verify the ordering against a test that cannot see auth rows", "C — file a separate finding card recording the misattribution" ], "recommendation": "B, because #10974 carries needs:contract-review and pm:blocking: a reviewer asked to sign off on published-surface widening will read that sentence as a verified mechanical claim and check the named file, find ROUTE_LEDGER, and either stall or wave it through on a misread. One sentence in the body removes that, and after PR #11403 the corrected citation is a REAL guard rather than a second aspiration. A leaves a live trap in the one card that is explicitly under review; C spends a card on a sentence edit. This is your call, not mine — I did not touch another card's reasoning." }, { "question": "#10975 (the follow-up that adds the `set-initial-password` ledger row, pm:blocked on #10974) requires its PR body to argue the `source: 'objectstack'` pin 'from 3 to 4'. That pin now holds ELEVEN routes — it grew in #10534 — so the acceptance criterion as written is against a number that no longer exists. Fix now or at dispatch?", "options": [ "A — edit #10975's body now, while it is blocked and nobody is on it", "B — leave it and correct it in the dispatch words when #10975 is dispatched", "C — leave it entirely; the dev will read auth-route-ledger.conformance.test.ts and self-correct" ], "recommendation": "A, because the stale number is inside the card's ARGUE-IT requirement rather than in background prose, and a dev who takes '3 to 4' literally will write a PR body asserting something the file contradicts — cheap to fix while the card is parked, and it costs nothing to also note there that PR #11403 now makes that row's `client` name mechanically resolved, which is new information for whoever writes it." } ], "out_of_scope_findings": [] }Generated by Claude Code