Skip to content

feat(spec): discovery reports which auth route families are mounted (authFamilies.admin) - #21145

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21046-discovery-auth-families
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21046-discovery-auth-families

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21046
Clause-②: yes

What this does

The #15920 ruling (maintainer 「同意」, ruling record 5564370232) keeps the plain 404 that an unmounted better-auth admin route answers. It names discovery as the place an SDK caller asks "does this deployment mount the admin family" before building those URLs. Discovery could not answer that. This PR adds the answer.

  • Spec (@objectstack/spec, minor, widening). DiscoverySchema gains an optional, closed authFamilies block, { admin: boolean }. It comes with AuthFamiliesSchema / AuthFamilies and the one reader both producers call, readAuthFamilies(authService). The reader returns getPublicConfig().features.admin, which is the object GET /api/v1/auth/config serves. It returns undefined (and the producer emits no key) when there is no auth service, when the service has no getPublicConfig(), when that call throws, or when the flag is not a boolean.
  • Producers (patch). getDiscovery() in @objectstack/metadata-protocol and getDiscoveryInfo() in @objectstack/runtime each emit authFamilies from readAuthFamilies. Neither one re-derives whether the admin plugin is on. The @objectstack/rest GET /api/v1/discovery composes over getDiscovery() and passes the key through unchanged.
  • Why only admin. It is the family the ruling discusses. The block is closed, so adding a family is a contract change, and the schema's docblock says what to check first. For sso, the /config route narrows the flag to "usable" after getPublicConfig() returns, so the raw flag answers a different question.

Repro: before and after

Measured with @objectstack/verify bootStack(showcase). The admin plugin is switched on with OS_SCIM_ENABLED=true, which forces it on (ADR-0134). Each column is one boot.

reading stock, base 9c8b65aa23 admin on, base 9c8b65aa23 stock, this branch admin on, this branch
GET /api/v1/discovery authFamilies absent absent { admin: false } { admin: true }
GET /.well-known/objectstack data.authFamilies absent absent { admin: false } { admin: true }
GET /api/v1/auth/config data.features.admin false true false true
anonymous GET /api/v1/auth/admin/list-users 404, empty body 401 UNAUTHENTICATED 404, empty body 401 UNAUTHENTICATED

On base, both discovery documents had the same key set in both boots, and routes.auth was the only auth fact. With this branch, both documents agree with /auth/config and with what the wire does.

Pins

  • packages/spec/src/api/discovery-auth-families.pin.test.ts: the key is declared and kept by both DiscoverySchema and the consumer parse GetDiscoveryResponseSchema. The block is optional and closed (['admin']). The reader reads getPublicConfig() on the service itself. It answers undefined, never a guessed false, for each of the unreadable cases.
  • packages/runtime/src/discovery-auth-families.pin.test.ts and packages/metadata-protocol/src/discovery-auth-families.pin.test.ts: each producer reports { admin } for both values of the service's public config. Each asserts that getPublicConfig was called and that the result equals readAuthFamilies(service). Each emits no key without an auth service, and none for a service with no public config. The runtime pin also covers a throwing config.
  • packages/qa/dogfood/test/discovery-auth-families.dogfood.test.ts (door pin, runs in the isolated project): two real boots, stock and admin-on. Each boot asserts four things agree: REST /discovery, the dispatcher's /.well-known/objectstack, /auth/config features.admin, and the wire (404 against non-404 on an admin-family route). The frame of each body is asserted, not tolerated. REST answers bare and the dispatcher answers enveloped, as recorded in rest-route-ledger.ts.
  • packages/spec/src/type-alias-convention.pin.test.ts: AuthFamiliesSchema is pinned as ADR-0122 isomorphic, 778 to 779. check:spec-parsed-alias named the new alias. The schema is a closed object of booleans, so the pin is the ADR's remedy rather than an AuthFamiliesParsed synonym.

Ablation (the fix committed first; every leg restored and proven)

Each producer was mutated to hard-code today's stock answer: const authFamilies = ['ablation-21046'].length ? { admin: false } : undefined;. The mutation went through scripts/ablation-replace.mjs. In each leg the anchor hit 1 to 0 and the blob changed. Restore was proven as blob equal to HEAD with an empty git diff HEAD.

  • Runtime unit leg: 5 of 5 red, then 5 of 5 green after restore. Before running, I predicted red for the true case and the two absent-key cases only. The false case went red too, on expect(getPublicConfig).toHaveBeenCalled(). So the pin also rejects a constant that equals today's answer.
  • Metadata-protocol unit leg: 4 of 4 red, then 4 of 4 green after restore.
  • Door leg (metadata-protocol, the REST /discovery producer): I rebuilt from the mutated source. The JS emitted; the DTS step failed on the now-unused import (TS6133), which is expected under this mutation. ablation-dist-preflight reported the marker present in 2 built files. Dogfood result: 1 red, 7 green. The red is GET /api/v1/discovery reports authFamilies.admin: true in the admin-on boot. The stock boot stayed green (false matches) and so did the dispatcher's .well-known (not mutated). After restore and rebuild, ablation-dist-preflight --absent reported the marker gone from all 24 built files and the tree clean. Dogfood was then 8 of 8 green.

Local verification (final head 155c7268ff)

  • Tests at 6b88aab411: spec --project local 591 files / 17367 tests passed. spec + runtime test:repo 47 / 832 and 3 / 751. runtime --project local 298 / 4254. metadata-protocol 197 files (3 skipped) / 2935. The dogfood door pin passed 8 of 8. The only commit after that (155c7268ff) edits one spec test file, the ADR-0122 pin. At 155c7268ff I re-ran that file together with the new spec pin and discovery.test.ts: 3 files, 102 tests, all passed (still declares all 779 isomorphic pins).
  • Typecheck at 155c7268ff: spec (tsc, check:scripts-typecheck, check:test-typecheck), runtime (tsc, check:test-typecheck), metadata-protocol and dogfood all passed.
  • Spec artifacts were regenerated from a built dist, and pnpm --filter @objectstack/spec check:generated reports all 15 up to date.
  • Gate families: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 110 commands at 155c7268ff, and I ran each one with its exit code captured before any pipe. Six spec gates first refused with exit 3 (check:api-surface, check:dual-source-exports, check:entry-nameability, check:exported-any, check:skill-examples, and check:generated's api-surface leg). The cause was a stale spec dist: the last commit edits a spec test file, which moves the dist input hash. I rebuilt spec at the same head and re-ran all six; each exited 0. --ran reconciliation: 110 derived famil(ies) accounted for — 110 run, 0 NOT-MEASURED, all exit 0. Along the way, check:spec-parsed-alias caught a real miss, the ADR-0122 alias, which is fixed in 155c7268ff.
  • Lint at 155c7268ff, as a proven narrowing rather than a full pnpm lint. (1) Population: the 8 changed .ts files all match eslint.config.mjs's files globs (packages/**/*.{ts,…} and **/*.{ts,…}). (2) eslint --no-inline-config --format json over them linted 8 files with 0 errors and 0 warnings. (3) Invariance: that config enables no type-aware linting (no parserOptions.project, no typed rules), and its local plugins read nothing but config-time baselines. This diff changes neither the config nor a baseline, so it cannot move the verdict on any file it does not touch.

Acceptance notes

  • Not merged with origin/main before opening. The branch is based on 9c8b65aa23. A local merge-tree probe against origin/main 39ab2940e2 (19 commits ahead) is clean. Upstream touched packages/metadata-protocol/src/protocol.ts (imports, a different region) but no file this diff generates. CI and the merge queue verify the merge ref.
  • Observation, not filed: IAuthService (packages/spec/src/contracts/auth-service.ts) does not declare getPublicConfig. Discovery now reads it structurally, as packages/adapters/hono already does for /auth/config. That is the "called, declared by nobody" shape the contract's own [dispatcher 多个 domain 调用契约里没有的方法 —— #4087 的同类,只是方向相反(契约缺声明,不是调用点乱编) #4127] notes describe. It was out of this card's file surface. Carrier: none.
  • Observation, not filed: GetAuthConfigResponseSchema.features (AuthFeaturesConfigSchema) does not declare admin, nor several other flags getPublicConfig() serves. A consumer that parses /auth/config through the spec strips features.admin. That makes this discovery key the only spec-declared carrier of the answer. The SDK's auth.getConfig() returns the body raw, so no shipped reader loses it today. Carrier: none.
  • #15920 is not reopened here: the 404 on unmounted admin routes is unchanged, as that ruling decided.

Generated by Claude Code

claude added 3 commits October 1, 2026 07:42
…authFamilies.admin)

DiscoverySchema gains an optional, closed `authFamilies` block. `admin`
says whether the better-auth admin family is mounted, read through the
new `readAuthFamilies` from the auth service's own getPublicConfig() --
the object GET /auth/config serves -- so neither discovery producer
re-derives whether the admin plugin is on. Both producers
(metadata-protocol getDiscovery, runtime getDiscoveryInfo) emit it;
REST /discovery passes it through.

Pins: spec reader/schema, both producer unit pins, and a dogfood door pin
that boots showcase stock and with the admin plugin on and checks the two
discovery documents against /auth/config and the wire.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
api-surface, export-origins, declaration-map, authorable-surface,
json-schema manifest, reference docs and the strictness-ledger count,
from a dist built at the previous commit (check:generated green).

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
check:spec-parsed-alias named the new `AuthFamilies` alias: a closed
object of booleans, so z.input === z.infer and the ADR's remedy is the
isomorphism pin, not an `AuthFamiliesParsed` synonym.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/spec, touching 6 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/kernel/services-checklist.mdx (via getDiscovery (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/protocol/kernel/http-protocol.mdx (via DiscoverySchema (symbol, a top-level const))
What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

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

Which tree this was computed on

This run read content/docs from 53b5c7083bf45d6e6189ad1a072bddf710fe29b9 — the merge of head 155c7268ff5e91c4f1a87bfe389c68130cc7ec00 into base b3917d9090b3401c545757a034e163b1cff9c0a1, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 53b5c7083bf45d6e6189ad1a072bddf710fe29b9 && git checkout 53b5c7083bf45d6e6189ad1a072bddf710fe29b9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b3917d9090b3401c545757a034e163b1cff9c0a1 155c7268ff5e91c4f1a87bfe389c68130cc7ec00 && git checkout -B drift-repro b3917d9090b3401c545757a034e163b1cff9c0a1 && git merge --no-ff 155c7268ff5e91c4f1a87bfe389c68130cc7ec00

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

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

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 155c7268ff5e91c4f1a87bfe389c68130cc7ec00
Local-runs: none

Inputs read: card #21046 (body, triage grade 5924648386, claim 5926796903, os-dev-report 5929029583), ruling 5564370232 on #15920, PR #21145 (body, the 18-file list at +630/−14, git diff 9c8b65aa23..refs/review/pr-21145), the head's check-runs collapsed latest-per-name, and the precedents at the head (discovery.zod.ts readers and DiscoverySchema members, ADR-0122 plus scripts/check-spec-parsed-alias.mjs, the WHICH LEVEL paragraph in .github/workflows/pr-automation.yml with scripts/check-changeset-no-major.mjs and scripts/pm/clause2-line.mjs, ADR-0076 D12). Branch is three commits on base 9c8b65aa23: 9d5ce4cda6 (feature), 6b88aab411 (regenerated artifacts), 155c7268ff (ADR-0122 pin).

① Derived judgments

(a) One source, no second derivation — holds.

  • The reader. readAuthFamilies(authSvc) (packages/spec/src/api/discovery.zod.ts:313-325) calls the service's own getPublicConfig() with this bound to the service and returns features.admin iff it is a boolean; otherwise undefined. It reads no plugin config and no environment.
  • Is that the exact object GET /auth/config serves? Yes. Two handlers serve that route at the head and both do the same thing: packages/adapters/hono/src/index.ts:606-619 and plugin-auth's own rawApp.get(basePath + '/config') at packages/plugins/plugin-auth/src/auth-plugin.ts:2220-2234. Each takes getPublicConfig(), refines only features.sso in place (to isSsoUsable()), and serves the object as { success, data }. admin is served raw. AuthManager.getPublicConfig() (auth-manager.ts:6599-6799) builds a fresh literal per call, so the in-place sso refinement cannot bleed into discovery's read, and features.admin = pluginConfig.admin ?? resolveScimEnabled(pluginConfig) (:6775) is the same resolver buildPluginList() mounts from, which is why the dogfood wire leg (404 against non-404) can agree with it. The dev's docblock warning that sso would NOT be raw-safe is accurate.
  • Both producers, and only those two. Runtime getDiscoveryInfo (packages/runtime/src/http-dispatcher.ts:1501) resolves authSvc through this.resolveService(kernel, CoreServiceName.enum.auth) on the request's own kernel (:1513-1519, the chain handleAuth uses) and calls readAuthFamilies(authSvc) once (:1785). Metadata-protocol getDiscovery (packages/metadata-protocol/src/protocol.ts:6396) reads this.getServicesRegistry().get('auth') and calls readAuthFamilies once (:6817). Every door routes through one of the two: REST GET /api/v1/discovery mutates routes/scoping on the getDiscovery() object and res.json(discovery) (rest-server.ts:4611, :4862), the hono adapter's /, /discovery and /.well-known/objectstack and dispatcher-plugin.ts:1042/1069 all serve getDiscoveryInfo. No third producer exists (git grep of getDiscoveryInfo( and .getDiscovery( outside tests). No re-derivation of "is the admin plugin on" anywhere in the diff.
  • Absent-key behaviour is the same at both doors: ...(authFamilies ? { authFamilies } : {}) in both producers, so no auth service, a service without getPublicConfig, a throwing config, or a non-boolean flag all emit no key, never admin: false. That matches the schema's .optional() and its docblock, the changeset's "When the key is absent" paragraph, and the pins (both unit pins assert hasOwnProperty === false; the runtime pin also covers the throw). The one asymmetry is deliberate and stated: a throwing config makes /auth/config answer 500 AUTH_CONFIG_ERROR while discovery stays answerable and claims nothing. Coherent with "a document a client connects through makes no claim it cannot read".

(b) The contract shape — accepted as shipped, with two product-contract flags recorded in ③.

  • Name. authFamilies and its member admin are the ruling's own words ("which auth families are mounted", "the admin family") and the member is the better-auth plugin name, the same vocabulary /auth/config features.admin and the auth-route-ledger's requires column use (requires: 'sso' | 'organization' | 'oidcProvider', "Optional better-auth plugin this route needs", auth-route-ledger.ts:109). It does NOT align with that ledger's family column, which is a different partition (core-auth, oauth-provider, objectstack-mount, organization, two-factor) with no admin member. camelCase config key, consistent with schemaDiscovery and scoping. I would not rename; whether "family" should mean the ruling's sense or the ledger column's is beyond the card's direction (flag).
  • Optional vs required. Both producers emit it whenever the auth service answers, and the file's own precedents cut both ways: the capabilities docblock (Ruling A, SDK 的 client.capabilities 声明为 WellKnownCapabilities,但两个 discovery 生产者填的是互不相交的键集 #5672) says a block every producer can answer is required, with enabled: false never omitted, while ServiceInfoSchema.handlerReady in the same file carries exactly the three-valued semantic the dev chose ("omitted = unknown / not yet verified; false = known missing"). The dev's choice is the honest one for the cases that actually arise: with no auth service routes.auth is absent too; with a non-AuthManager occupant or a throwing config the family may be mounted, so a forced false would advertise as absent a family that exists, the reverse of the Route and surface ownership rule 4. Optional is defensible and documented in the schema docblock; a maintainer reading Ruling A strictly could want required with { admin: false } on a no-auth boot, which is a one-line change plus a ruling for the two genuinely unknowable cases (flag, not a defect).
  • Closed. Right: the capabilities precedent (closed object, "a new one is a contract change"), pinned as ['admin'], with the docblock saying what to check before adding sso. Adding a family later is additive.
  • A runtime function in a .zod.ts file. Within the file's conventions: readServiceSelfInfo, readChannelRoute, isSubscribableChannel and resolveDiscoveryEnvironment are all exported runtime readers in discovery.zod.ts, and ADR-0076 D12 names readServiceSelfInfo as the pattern. readAuthFamilies is readChannelRoute with a try/catch added (the precedent has none) and the reason stated. One divergence worth noting: readChannelRoute reads a member IRealtimeService declares, readAuthFamilies reads a member IAuthService does not (out-of-scope finding 1 in ③).
  • Type alias. AuthFamilies = z.input (ADR-0122 D1); no AuthFamiliesParsed because the shapes coincide (D3); pinned isomorphic instead. Correct.
  • Consumer parse. GetDiscoveryResponseSchema is DiscoverySchema.partial().required({ version }).extend({ apiName }) (protocol.zod.ts:177), so the key flows to the consumer parse automatically and the 两个 discovery 生产者都在线上返回 schema 未声明的顶层字段(scoping / features / endpoints),且 REST 形状永远无法通过 DiscoverySchema #4828 key-set gate (discovery.test.ts:1096-1104) derives from the shape, nothing hand-listed to update.

(c) Pins — they hold the triage pin (off by default, on with admin, equal to /auth/config).

  • Spec pin (packages/spec/src/api/discovery-auth-families.pin.test.ts): declared and kept by DiscoverySchema and GetDiscoveryResponseSchema; optional; admin required-boolean when present; shape exactly ['admin']; the reader in both directions, carries only declared families, calls getPublicConfig on the service itself, and answers undefined for ten unreadable shapes including a throw.
  • Runtime pin and metadata-protocol pin: for admin in [false, true] the document says { admin }, getPublicConfig was called, and the value equals readAuthFamilies(auth); no key without an auth service and none for a service without public config; the runtime pin also covers a throwing config. The toHaveBeenCalled assertion is what makes the dev's U1/U2 ablation claim structurally true: a hard-coded { admin: false } reds the false case as well as the true one. Not re-run here; the assertion structure supports the reported 5/5 and 4/4 reds.
  • Dogfood door pin (packages/qa/dogfood/test/discovery-auth-families.dogfood.test.ts): two real showcase boots via @objectstack/verify (stock; admin-on through OS_SCIM_ENABLED=true, the ADR-0134 forcing the sibling admin-route-nonadmin-refusal test already relies on), four readings per boot (REST /discovery bare, .well-known enveloped, /auth/config features.admin, anonymous /auth/admin/list-users status), the frame of each body asserted not tolerated. Lands in the isolated vitest project (every file minus SHARED_SHOWCASE), which is where a process.env toggle belongs. D1 (metadata-protocol mutated, rebuilt) reporting exactly one red, REST admin: true in the admin-on boot, is the expected signature of that mutation; not re-run here.

(d) Generated artifacts and the ADR-0122 registration — consistent, nothing hand-edited, nothing extra.

  • api-surface/api.json +3 (AuthFamilies (type), AuthFamiliesSchema (const), readAuthFamilies (function)), export-origins/api.json +3 with the right origins, declaration-map/api.json +2, json-schema.manifest/api.json +1 (api/AuthFamilies), authorable-surface/api.json +3 (api/AuthFamilies:admin, api/Discovery:authFamilies, api/GetDiscoveryResponse:authFamilies). Each is exactly what one new exported schema plus one new key on DiscoverySchema (flowing into GetDiscoveryResponse) produces.
  • content/docs/references/api/discovery.mdx, api/protocol.mdx (the nested GetDiscoveryResponse.authFamilies shape) and index.mdx (1521 to 1522 schemas, API 429 to 430) carry the AUTO-GENERATED banner and only the rows the source change produces; Build Docs is green.
  • Strictness ledger docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md 432 to 433: the file is AST-computed (packages/spec/scripts/build-strictness-ledger-counts.mts, one count per z.object site under src/api/) and the diff adds exactly one z.object site (AuthFamiliesSchema); authFamilies: itself is a reference, not a site. Correct.
  • type-alias-convention.pin.test.ts: one Iso_api_discovery__AuthFamiliesSchema line, the prose count 778 to 779 in both places the runtime companion checks, and a ledger note. This is precisely arm 2 of scripts/check-spec-parsed-alias.mjs (ADR-0122 D7: a bare z.input alias needs XParsed or an isomorphic pin in this file), and D3 forbids the XParsed synonym for an isomorphic schema, so the pin is the only correct remedy.

② Semver level

  • @objectstack/spec minor with Clause-②: yes is right: a new accepted key on a published payload plus three new exports on the api entry is the additive widening WHICH LEVEL says takes at least minor, and the level axis in check-changeset-no-major.mjs (declared yes requires at least one moved package at minor) is satisfied by spec; Check Changeset is green.
  • The PR body's bare Clause-②: yes is a legal spelling. scripts/pm/clause2-line.mjs states the closed set: "yes / yes (widening) — a widening. The second spelling is the first, said out loud; both take at least minor", and the arm is optional by measurement. No need to rewrite it as yes (widening).
  • @objectstack/runtime and @objectstack/metadata-protocol at patch: neither index gains a new exported symbol and neither accepts a new key. HttpDispatcher and ObjectStackProtocolImplementation are exported from their entries and both methods have inferred return types, so each package's .d.ts gains an optional member on an existing return type, carried by the spec's declared AuthFamilies. That is not one of the acts the rule names; patch matches the claim and the triage grade and is accepted.
  • The changeset (.changeset/21046-discovery-auth-families.md) is accurate: three packages at those levels, the Clause-②: yes line the ADR-0087 gate reads, the absent-key semantics, the /auth/config equivalence, and "what did not change" (the 404 on unmounted admin routes stands, as the ruling decided). No @objectstack/rest file moved, so no rest entry is owed.

③ Boundary flags

Deviations (each read against the head):

  • ADR-0122 pin edit outside the literal surface: required by the check:spec-parsed-alias gate, which the dev reports red at 6b88aab411 and green at 155c7268ff; the dispatch delegated to gates that require registration; one line plus the count. Accepted.
  • Spec surface beyond one key: the exported reader is the readChannelRoute precedent's shape and is what makes "no second derivation" a mechanism both producers import rather than a convention; the claim already declared the spec a cross-lane surface with Clause-②: yes. Accepted.
  • No merge with origin/main: probed here against a freshly fetched origin/main e952cff578 (25 commits ahead of base). git merge-tree --write-tree origin/main refs/review/pr-21145 is clean, both in this clone and from a driverless bare probe clone, same tree cd06744c97. Upstream touched packages/metadata-protocol/src/protocol.ts in three commits (feat(metadata-protocol): a stored page naming an absent plugin is reported at load, and draft promotion re-stamps requires #21121, fix(metadata-protocol)!: stored metadata bodies on the generic data door are served as their type's read projection (#21086) #21115, fix(rest,runtime): the published-snapshot doors answer the package's flow for a shipped flow name with a stored row, as the layered read does (#21002) #21116) in other regions, and touched spec source in five commits whose regenerated artifacts are authorable-surface/ui.json, references/ui/component.mdx and references/marketplace/package.mdx only: no api.json shard, no references/index.mdx, no counts file and no alias pin, so the PR's regenerated artifacts stay byte-exact on the merge ref and no check:generated red is expected there. The queue rebuilds that ref.
  • Full suites at 6b88aab411 not the final head: the only later change is the pin test file, re-run at the head by the dev, and CI at the head is green across Test Core, Dogfood, Type Check and Lint. Closed by CI.
  • The trailer pair: all three commits carry the model-free Claude-Session plus Co-authored-by: Claude pair AGENTS.md requires; the harness reminder's model-named trailer is correctly refused, and the harness-written exemption covers reporting only. Correct.
  • DTS failure under ablation (TS6133 on the import the mutation orphans): expected for that mutation; the JS emitted and the dist preflight proved the marker before the dogfood read. Accepted, not re-run.

Out-of-scope findings:

Closing and surfaces:

  • Fixes #21046 closes the card correctly: scope line 1 delivered (both doors report the admin family from the source /auth/config reads, no second derivation), scope line 2's decision path not triggered (measurement showed discovery can read the same source without a plugin-auth fork), the pins delivered on real boots. The card this PR closes must claim this branch and Part-of PR must not also close its card are both green.
  • packages/plugins/plugin-auth/** untouched and content/docs/releases/** untouched (18-file list). No governed-surface path; 644 changed lines; head repo equals base repo.

Escalation flags (product-contract choices beyond the card's direction, recorded, not decided here): the key name authFamilies/admin follows the ruling's words and the features/requires plugin vocabulary rather than the ledger's family column; the block is optional with absent meaning "not known to be mounted" (the handlerReady reading) rather than required with false (the Ruling A reading). Both are stated in the schema docblock and neither blocks the card.

Check-runs on the head (converged, 35 names collapsed latest-per-name): 33 success, 2 skipped (Console Pin Gate, path-filtered with no .objectui-sha change; Packed-tarball smoke (opt-in)), 0 failure, so there is no red to compare against origin/main. The seven required contexts are all success: Lint & Repo Gates (10:06:48Z), TypeScript Type Check (10:07:34Z), Test Core (10:10:45Z), Dogfood Regression Gate (10:00:59Z), Build Core (09:58:33Z), Temporal Conformance (live PG + MySQL) (10:02:19Z), Governed Surface Queue Guard (09:52:00Z). Also green: Check Changeset, Spec property liveness, Build Docs, Dogfood Verify CLI, the four Type Check legs and the claim gates.

Implemented-by: claude/issue-21046-discovery-auth-families
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 10:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 10:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 70dae53 Oct 1, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21046-discovery-auth-families branch October 1, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants