Skip to content

fix(plugin-auth): a refused auth setting no longer drops the settings saved with it - #20429

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20412-auth-settings-sibling-isolation
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20412-auth-settings-sibling-isolation

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20412

Clause-②: no

What changed

AuthPlugin's auth settings pass (bindAuthSettings → applySettings, packages/plugins/plugin-auth/src/auth-plugin.ts) used to send the whole namespace to AuthManager.applyConfigPatch() as ONE patch inside one outer try. When the manager refused one key, the whole patch was dropped (password policy, MFA, rate limits, session lifetime, social providers) and the only trace was ctx.logger.warn('Auth: failed to apply auth settings: …'), while the settings console showed every value as saved.

Seat ruling applied: a refused key does not take its accepted siblings with it.

  • The pass is now applied in pieces, split along the lines where the manager can refuse. applyConfigPatch validates a patch on entry wherever it carries emailAndPassword (assertAudienceConfig, against the standing audience posture) or plugins (assertScimAdminCoherence). Each settings key that lands in one of those blocks is applied ALONE. Every other key goes out in ONE application that the manager does not validate.
  • mfa_required: true stays one piece with the twoFactor plugin it turns on. A refused plugins block therefore leaves MFA at its standing value too, and MFA is never enforced without its enrollment endpoints.
  • Every piece has its own try. A refusal is logged ONCE at error, naming the key, saying what keeps ruling and what the console shows, followed by the manager's own message, which carries the remedy. This follows the audience block beside it: [auth] auth settings REFUSED (auth.require_email_verification) — the standing runtime value keeps ruling while the settings console shows the stored value as saved; the other auth settings in this pass still apply. [audience] invalid audience configuration: posture 'open' FORCES email verification on unless the DEPLOYMENT turns it off — …
  • The outer catch is left with only a pass that fails as a whole, the namespace read first among them. It moves from warn to error ([auth] auth settings NOT APPLIED — …) for the same reason: stored values the console shows as saved were not applied. This is the same line the card names as the defect.
  • The plugin decides nothing. The manager's verdict on each piece is the verdict, and no validation is duplicated in the plugin. The audience block, its order (after the pieces, so it is judged against the verification state this pass just applied) and its messages are unchanged.

Premise check (the ruling's precondition), measured before writing the fix

The refusal can be attributed without re-implementing validation. applyConfigPatch (auth-manager.ts, around line 4162 at 40b315b0) runs exactly two entry validators, each gated by which top-level block the patch carries. Splitting the pass along that gating gives each refusable key its own verdict from the manager. It needs no knowledge of what the validators check. Per-key isolation yields the same accept/refuse verdict as the whole-block application for every key on this tree: assertAudienceConfig reads only requireEmailVerification from emailAndPassword, and assertScimAdminCoherence reads only admin/scim, which this pass never sets. So nothing that used to be accepted is refused, and the refused key is refused for the same reason. Landing point: the plugin (the consumer that composed the one patch), not auth-manager.ts.

Live triggers, measured on main 40b315b0 before the fix (new pins run against unmodified auth-plugin.ts)

src/auth-settings-refusal-isolation.test.ts drives the real AuthPlugin + AuthManager through a stub settings namespace read:

trigger on 40b315b0
console door, no stack config: posture opened through the console, then require_email_verification: false saved with siblings session.expiresIn stays undefined (every sibling dropped)
stack-config open + console-stored false at boot session.expiresIn undefined
stack-config email_domain + env (OS_AUTH_REQUIRE_EMAIL_VERIFICATION) false session.expiresIn undefined
plugins SCIM/admin coherence refusal (OS_SCIM_ENABLED appearing after construction with plugins.admin: false) session.expiresIn undefined
failed namespace read logged at warn only
CONTROL: clean pass green

Result: Tests 5 failed | 1 passed (6), with every negative pin failing on expected undefined to be 259200 / 604800 or on the missing error line.

The settings service has no cross-field validation between require_email_verification and audience_posture (packages/services/service-settings/src/manifests/auth.manifest.ts). The first trigger is therefore reachable by two console saves, with no stack config or env involved.

Surface beyond the claim's file list (declared)

  • scripts/check-durability-degradation-log-level.mjs: applyConfigPatch joins DURABILITY_CRITICAL_CALLEES (AGENTS.md, Degradation log levels: "found a new one, add it to DURABILITY_CRITICAL_CALLEES in the same PR that fixes it"). Seams 36 → 38, all loud. With auth-plugin.ts at 40b315b0, the gate reds on the old warn catch (ablation below).
  • scripts/measure-durability-swallow-family.mjs: that census's by-value gate-vocabulary copy gains the same name. check:swallow-census-controls reds on the drift otherwise, measured: "declared by the gate, missing from the copy: applyConfigPatch".
  • scripts/engine-double-contract.pinned.json: node scripts/check-engine-double-contract.mjs --write for the new test file's findOne double. The double is copied from audience-posture-setting.test.ts, which is already pinned. The gate itself asked for this: "New pinned coverage is GOOD … Run --write and commit". The result was one added row and 0 seam rows lost.

No packages/spec, no service-settings, no auth-manager.ts change.

Tests

  • New packages/plugins/plugin-auth/src/auth-settings-refusal-isolation.test.ts (6 tests):
    1. with a live refusing key in the pass, siblings from every family ARE applied: password_min_length (same emailAndPassword block), password_require_complexity, mfa_required + twoFactor, rate_limit_max, session_expiry_days, Google social provider;
    2. the refusal is ONE error line naming the key and carrying the manager's remedy text, and the old warn never fires;
    3. CONTROL: a clean pass applies everything and reports no refusal;
    4. the plugins-block refusal names each key it carried (password_reject_breached, mfa_required) and keeps MFA with its plugin;
    5. a failed namespace read is one error line (auth settings NOT APPLIED).
  • pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2: Test Files 115 passed (115), Tests 2460 passed (2460) (at 846b2858; later commits touch only scripts/ ledgers and the census copy).
  • pnpm --filter @objectstack/plugin-auth run typecheck (after pnpm --filter @objectstack/plugin-auth build): exit 0. The new test file is in the tsconfig.test.json program (--listFiles: 1 hit), and check:test-typecheck: OK.
  • pnpm exec eslint --no-inline-config --format json on the four changed lintable files: 4 files, 0 errors, 0 warnings. The population is read from eslint.config.mjs (files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] minus NEVER_LINTED; none of the four reported as ignored). Invariance: type-aware linting is not enabled anywhere in eslint.config.mjs (no parserOptions.project, no projectService), so this diff cannot move any untouched file's verdict. The repository-wide pnpm lint is CI's.

Ablations (the fix committed first; each leg's restore proven by blob hash plus an empty git diff HEAD)

  1. The old whole-pass drop put back. auth-plugin.ts was restored to 40b315b0 (tree only). Proven on disk: the blob equals the base blob dd862c8d, the old warn count is 1 and the applyPiece count is 0. The new pins went red, Tests 5 failed | 1 passed (6) (four on expected undefined to be 259200 / 604800, one on the missing error line; the control stayed green). check:durability-log-level went red: ✗ 1 durability report(s) degrade quietly … packages/plugins/plugin-auth/src/auth-plugin.ts:1799. Restored to the HEAD blob 45e547d5.
  2. Isolation kept, refusal logged at warn (node scripts/ablation-replace.mjs, anchor hit 1 → 0, blob 45e547d5 → 0a104f02). The pins went red, Tests 4 failed | 2 passed (6): each fails at expected [] to have a length of 1 (2 for the plugins test). The siblings still applied, so this leg isolates the level pin. check:durability-log-level went red at auth-plugin.ts:1767. Restored to 45e547d5 with git diff HEAD empty.

Gates (derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at the final head, 86 commands, all run at 950c40c5)

  • 84 exit 0, including check:durability-log-level, check:swallow-census-controls, check:engine-double-contract, check:logger-receiver-detach, check:doc-authoring, check:nul-bytes, check:issue-citations, check:adr-0087-registration --base origin/main, check:changeset-no-major and check:pm-dispatch-gates (its battery took 1026.8s on this shared box).
  • NOT MEASURED: check:dual-build-cjs-loads. Reason: PREREQUISITE NOT MET (exit 3), because the gate reads the built output of every workspace package and 39 have no dist/ in this worktree. A targeted probe: plugin-auth's own dist/index.js (CJS require) and dist/index.mjs (ESM import) both load, and AuthPlugin is a function.
  • NOT MEASURED: check:type-check-debt. Reason: PREREQUISITE NOT MET (exit 3), because the ledgered packages runtime, service-cluster and service-job have no built type entry. This diff touches none of them. plugin-auth's own typecheck and check:test-typecheck are green above.
  • Also run, because the diff edits their inputs: check:optional-error-sink, check:startup-registry-verdict, node scripts/measure-return-propagating-durability-seams.mjs, node scripts/measure-durability-swallow-family.mjs --self-test (all families), and packages/metadata-protocol sys-metadata-repository.draft-drain.test.ts, which reads the gate file (11/11). All exit 0.

Acceptance notes

  • Order inside one save (observation, pre-existing, not changed here). The verification piece is still judged against the standing posture before the same pass's audience declaration is applied. The audience block's comment records this order as deliberate. Take one save that both closes the posture (open → invite_only) and stores require_email_verification: false. That save logs one REFUSED line for the key. The effective requirement already matches (under invite_only an undeclared value resolves to off), and the next pass accepts the stored false. Before this change the same save dropped the whole pass at warn. No one else is carrying this: noted, not filed.
  • Reach of the plugins refusal. It is reachable from this pass only when OS_SCIM_ENABLED appears after AuthManager was constructed with plugins.admin: false. The constructor refuses an incoherent config at boot, and buildPluginList() separately refuses the lazy better-auth build in that state. The pin holds the MFA/twoFactor coupling and the per-key naming, and it is not a claim of common reach.
  • A refused value that stays stored is re-reported on every later pass (each settings change re-runs the pass), the same cadence as the audience block's REFUSED line.
  • Log text change for operators, stated in the changeset: Auth: failed to apply auth settings: is gone. It is replaced by [auth] auth settings REFUSED and [auth] auth settings NOT APPLIED.

Generated by Claude Code

… siblings

The auth settings pass is applied in pieces split along the lines where
AuthManager.applyConfigPatch can refuse: each key that lands in the
emailAndPassword or plugins block is applied alone, every other key rides
one application the manager does not validate. A refusal is reported once
at error, naming the key, and the rest of the pass still applies. A pass
that fails as a whole (the namespace read) is reported at error too.

Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs
Co-authored-by: Claude <noreply@anthropic.com>
…he auth settings pass

applyConfigPatch joins the durability-critical callee vocabulary: a refused
patch leaves the settings console showing a value the runtime never
applied, so its catch may not regress to warn.

Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs
Co-authored-by: Claude <noreply@anthropic.com>
…-vocabulary copy

Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 13 documentable anchor(s).

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

  • content/docs/deployment/self-hosting.mdx (via emailAndPassword (literal, a string literal in bindAuthSettings))
  • content/docs/permissions/authentication.mdx (via mfa_required (literal, a string literal in bindAuthSettings), password_reject_breached (literal, a string literal in bindAuthSettings))
What this run could not see
  • 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 — 15 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 24b7085939ddee31fbeb59c049b0576cf51acf37 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 90165465b613a4a980ac920cc7d735b80d11b108 — the merge of head 950c40c51df7bcc81c9988a3e9ccd957140b6c65 into base 24b7085939ddee31fbeb59c049b0576cf51acf37, 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 90165465b613a4a980ac920cc7d735b80d11b108 && git checkout 90165465b613a4a980ac920cc7d735b80d11b108
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 24b7085939ddee31fbeb59c049b0576cf51acf37 950c40c51df7bcc81c9988a3e9ccd957140b6c65 && git checkout -B drift-repro 24b7085939ddee31fbeb59c049b0576cf51acf37 && git merge --no-ff 950c40c51df7bcc81c9988a3e9ccd957140b6c65

node scripts/docs-audit/affected-docs.mjs --json 24b7085939ddee31fbeb59c049b0576cf51acf37

⚠️ 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 24b7085939ddee31fbeb59c049b0576cf51acf37 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 950c40c51df7bcc81c9988a3e9ccd957140b6c65
Local-runs: none

Inputs read: card #20412 (body + all 3 comments: claim 5867200492, os-dev-report 5868596370, seat ACCEPT 5868627018), PR #20429 body, its 6-file list, the net diff against main 24b70859 (base of the PR = origin/main at reading), the check-runs on the head (three polls, last at 2026-09-28T11:13:45Z), and file content at the head and at origin/main via git show / git grep on a fetched review ref (no checkout). auth-manager.ts is byte-identical at the head and at main, so every judgment below about the manager reads the unchanged validator.

① Derived judgments

The security question first — does any split change WHICH values the manager accepts or refuses, and can any piece now land a combination the whole-patch validation refused as incoherent. Read off applyConfigPatch (auth-manager.ts 4162-4239, unchanged): it shallow-merges emailAndPassword and plugins into the standing config, replaces audience and socialProviders whole, then runs exactly two entry validators, each gated on which top-level block the PATCH carries — assertAudienceConfig(next.audience, next.emailAndPassword, declarant) when the patch carries audience or emailAndPassword, and assertScimAdminCoherence(next.plugins) when it carries plugins. Nothing else in it throws.

  1. The unvalidated patch piece cannot be refused — RIGHT. At the head, patch never receives emailAndPassword (every field goes through emailAndPasswordField into refusable), never plugins (password_reject_breached and mfa_required: true are refusable pieces; mfa_required: false sets only patch.mfaRequired), never audience (separate block, unchanged). So neither validator runs on it, exactly as the PR claims. Its applyPiece catch is a defensive dead branch; its key list names every explicit key not in a piece, including keys that contributed nothing (a malformed password_min_length, an invalid membership_policy) — cosmetic and unreachable, not wrong.
  2. The four emailAndPassword pieces that do not carry requireEmailVerification (email_password_enabled, signup_enabled, password_min_length, password_max_length) are always accepted — RIGHT, and no narrowing. assertAudienceConfig reads from emailAndPassword only requireEmailVerification (audience-posture.ts 295-312). The declarant is recomputed from options.requireEmailVerificationFrom ONLY when the patch's emailAndPassword carries requireEmailVerification (auth-manager.ts 4213-4218); otherwise the standing deploymentDeclaredVerificationOff is kept. So a piece applied with options undefined is judged as (standing audience, standing rEV, standing declarant) — the standing state, which is self-consistent by construction (the constructor validates the deployment declaration at 1391-1392; every accepted patch validated its merged result). My initial concern — that a piece without the from option would be judged as console and refuse a previously accepted key when the deployment had turned verification off under open — does not hold on this code: the standing flag rules for such a piece.
  3. The require_email_verification piece gets the same verdict the whole patch got — RIGHT. Same standing audience (the audience block still runs after the pieces), same value, same from derivation (sources.require_email_verification === 'env' ? 'deployment' : 'console', moved verbatim from the old whole-patch call), same merged rEV. The three refusals — false under email_domain from any source, a console false under open, and the merged-audience refusal when a pass OPENS beside a console false — are unchanged. A deployment false under open (env source, or a stack-config false with a later console false agreeing with it) is still accepted.
  4. The two plugins pieces get the whole block's verdict — RIGHT. assertScimAdminCoherence reads only admin and scim from the merged block (auth-manager.ts 398-411, resolveScimEnabled = scim ?? OS_SCIM_ENABLED ?? false); neither { passwordRejectBreached } nor { twoFactor: true } carries them, so each piece is judged on the standing admin/scim plus env, as the whole block was. Keeping mfaRequired: true in the same piece as twoFactor: true is RIGHT: a refused plugins block leaves MFA standing, so MFA is never enforced without its enrollment endpoints (test 4 pins mfaRequired and twoFactor both undefined after the refusal).
  5. No piece can land a combination the whole-patch validation refused as incoherent — RIGHT. The whole patch was refused in exactly three cases (rEV false under email_domain; console rEV false under open; standing admin: false with effective SCIM). In each, the refusal is attributable to one key's own verdict, because the validators read no other key from this pass — not enabled, disableSignUp, minPasswordLength, maxPasswordLength, mfaRequired, session, rateLimit, socialProviders, passwordRequireComplexity, membershipPolicy, nor any of the session/lockout/IP keys. There is no cross-key invariant on this tree under which "rEV refused, siblings applied" is an incoherent state the old validation protected against; the old drop was collateral, which is the card's defect. The PR's premise ("per-key verdicts equal whole-block verdicts") is measured correctly. Conversely nothing previously accepted is now refused (items 2-4). The accept-set of the manager is unchanged in both directions.
  6. Sequential pieces accumulate — RIGHT. The manager's shallow merge of emailAndPassword and plugins means a later piece does not wipe an earlier one; the CONTROL pin holds requireEmailVerification: true and minPasswordLength: 12 together after two emailAndPassword pieces. Coverage note, not a defect: two ACCEPTED plugins pieces both surviving (passwordRejectBreached plus twoFactor) is not pinned by the new file — correct by the manager's merge, and nothing in the diff can break it.
  7. No torn intermediate state — RIGHT. After the single await settings.getNamespace('auth'), the mapping, every applyPiece, and the audience block are synchronous; this.config is never observed mid-pass, and the instance invalidation (this.auth = null) fires on the first piece and no-ops on the rest. Order relative to the audience block (pieces first, audience judged against the rEV state this pass applied) is preserved.
  8. Log levels — RIGHT. The per-piece refusal is error with consequence (standing value rules, console shows stored) and remedy (the manager's own message) in one line, logged once per refused piece; the outer catch moves to error (NOT APPLIED) with consequence and fix. Both match AGENTS.md's durability/consistency question (persisted and runtime state disagree while nothing looks broken). The leg-1 ablation shows the durability gate also judges the outer catch, since it guards the audience block's applyConfigPatch, so error there is required by the widened vocabulary, not optional. No tracker number in any runtime string.
  9. Public surface — RIGHT: none moved. No export, option, schema key, error code or route changes; AuthPlugin and AuthManager signatures untouched. The operator-visible change is the retired warn line Auth: failed to apply auth settings: replaced by two error lines; the changeset states it with the alert-matching migration. At the head, no doc names the old line (git grep: only the changeset and two tests asserting its absence); ADR-0069 names bindAuthSettings → applyConfigPatch as the channel and says nothing about the pass being atomic, so no doc is falsified. The Docs Drift advisory's two pages match on literals (emailAndPassword, mfa_required, password_reject_breached) whose meaning this diff does not change.
  10. Gate vocabulary widening — RIGHT. applyConfigPatch joins DURABILITY_CRITICAL_CALLEES and the census's by-value copy, as AGENTS.md prescribes for a seam found and fixed in the same PR. Non-test call sites at the head are the two in auth-plugin.ts (both now at error) and the definition; the four packages/qa/dogfood/test callers sit outside any catch. One findOne ledger row for the new test, written by the gate's own --write — RIGHT.
  11. Residual, not wrong (the PR's Acceptance note). One save that closes open → invite_only AND stores a console require_email_verification: false logs one REFUSED line for that key (judged against the standing open) and the false lands on the next pass. Before this PR the same save dropped the posture change too, because the throw escaped to the outer catch before the audience block ran; the head is strictly better and the residual is loud and names the remedy.

② Semver level

@objectstack/plugin-auth (published, private unset, 17.4.0 at main) takes a patch changeset; no skip-changeset label on the PR; Check Changeset green. RIGHT: runtime behaviour of a released package moves (accepted siblings now apply; a log line changes text and level) while nothing authorable, exported or on the wire changes — item 9. The PR body and the changeset body each carry the line Clause-②: no at line start, with no (widening)/(narrowing) arm — RIGHT, by items 2-5 the accept-set is unchanged in both directions; no is the honest arm and no ADR-0087 marker is owed on a non-breaking changeset. The changeset text names the three reachable triggers, the piece rule, the MFA coupling and the alert migration; it matches the diff.

③ Boundary flags

  • open_questions: empty — nothing to answer.
  • Dev deviation, surface widened to three scripts/ files: the seat amended the claim in its ACCEPT (item 4 there); the widening is the rule's own prescription and the census copy and ledger row are gate-prescribed consequences — answered, right (① item 10).
  • Dev deviation, outer catch warn → error: answered, right (① item 8).
  • Dev deviation, attribution form: all four branch commits end with the model-free pair AGENTS.md names (Claude-Session: URL plus the bare Co-authored-by: Claude trailer) and the PR body ends with the session-URL footer; AGENTS.md wins over the harness reminder in this repo and the pre-push hook refuses a model identifier in the pair — answered, right.
  • Dev deviation, check:pm-dispatch-gates run detached: read from its log at the head, exit 0; CI is the authority — answered.
  • Two families NOT MEASURED locally on declared prerequisites (check:dual-build-cjs-loads, check:type-check-debt): CI answers — Type Check · debt ledger success, Build Core success; the repo-gates leg is in Lint & Repo Gates, still running at my last read (below).
  • Out-of-scope finding (ordering inside one save, carrier none): judged ① item 11 — noted, not escalated; the pre-change behaviour was strictly worse and the residual is loud.
  • Check-runs on 950c40c5 at 2026-09-28T11:13:45Z: 28 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 2 in progress (Lint & Repo Gates, Test Core (5/6)), 0 failure. Stated as read: the two running checks are not yet verdicts; the contract verdict below does not depend on them, the Tier S landing's every-check-green condition does.

Implemented-by: claude/issue-20412-auth-settings-sibling-isolation
Reviewed-by: session_017B6YKCGu8CTY2KBWgwaHAs

VERDICT: PASS

Rendered by an isolated contract-review subagent and adopted by the domain:services seat (#6021, session_017B6YKCGu8CTY2KBWgwaHAs) at 2026-09-28T11:16Z after a transcript check: 69 harness model stamps, all at CONTRACT_REVIEW_TIER, zero fallbacks; 34 Bash and 1 Read calls, one Write confined to its own scratch record, zero GitHub writes.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 11:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 7d63088 Sep 28, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20412-auth-settings-sibling-isolation branch September 28, 2026 12:06
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…tudio's metadata list and quick-find show them (objectstack-ai#20541)

Part of objectstack-ai#20299
Clause-②: no

Stage 1 of objectstack-ai#20299. Three docs-shaped liveness rows move `dead` → `live`,
because Studio already shows them to a human: `flow.description`,
`hook.label` and `hook.description`. The other four rows of the family
need objectui code and are objectstack-ai/objectui#11027's:
`app.areas.description`, `permission.rowLevelSecurity.label` /
`.description` and the `view` container `label`. They are untouched
here, and objectstack-ai#20299 remains open for them.

Ledger data, two README Notes cells and one gate-test fixture only. ⛔ No
schema, parse, `.describe()` or accept-set change. None of the three
rows sets `authorWarn`, so the set of lint warnings does not change. The
re-grade reverses no ADR-0033 decision: all three stay docs-shaped,
deliberately KEPT and exempt from enforce-or-remove. Each row keeps the
note it carried while `dead`, labelled as history.

## Files

- `packages/spec/liveness/flow.json`: the `description` row.
- `packages/spec/liveness/hook.json`: the `label` and `description`
rows, plus one dated sentence appended to the file `_note`. Its
2026-08-10 lookup ("both verdicts stand unchanged") is now history.
- `packages/spec/liveness/state-counts/flow.md` and
`packages/spec/liveness/state-counts/hook.md`: the two per-type count
shards, regenerated by `gen:liveness-counts`, not hand-edited. `flow`
has 35 live and 5 dead (was 34 / 6). `hook` has 21 live and 1 dead (was
19 / 3). Across all shards, the read-time sum that `check:liveness`
prints is 952 live and 136 dead (was 949 / 139). No file commits that
total.
- `packages/spec/liveness/README.md`: the `flow` and `hook` Notes cells.
Both listed these keys as dead.
- `packages/spec/scripts/liveness/check-liveness.test.ts`: the "stays
GREEN when a `dead` entry carries the SAME rotted pointer" case borrowed
`flow.description` as its sample `dead` row. It now uses the
`flow.active` tombstone, which the gate itself holds at `dead`, and it
asserts that precondition.
- `.changeset/20299-display-annotations-ledger.md`: a `patch` changeset
for `@objectstack/spec`. The ledgers ship in its `files[]` (`liveness`),
and `@objectstack/lint` reads them.

**File-surface declaration.** The claim's surface named `flow.json`,
`hook.json`, the regenerated counts and the changeset. The README cells
and the test fixture are outside it. The dispatch's pin sweep required
both to move with the flip ("grep for any test, doc or ledger note that
asserts these three rows are `dead` … and move it with the flip").
Without the fixture move, the gate test goes red: see the reverse
verification below. The README ships in the same `liveness` directory.

## The lane's call, and the ruling it applies

A Studio list column and the metadata quick-find count as consumers of a
DISPLAY-shaped key. That is the objectstack-ai#7131 ruling's table in
`packages/spec/liveness/README.md` ("Designer previews count as
consumers"): for a display key, being shown to a human is the whole of
the claimed effect. The README's `producer` discipline binds too, so
each row names who hands the reader a record.

## Premise, reader half: measured at the `.objectui-sha` pin `dd3f7e1be`

Instrument: read-only `git -C ../objectui show SHA:PATH` and `git grep …
SHA`. Nothing in objectui was edited, checked out or stashed.

- The Studio route `metadata/:type` mounts `MetadataResourceListPage`
(`packages/app-shell/src/console/AppContent.tsx`, both the with-app and
the zero-app branch).
- `MetadataResourceListPage` renders a registered custom `ListPage` if
one exists, and otherwise `DefaultMetadataList`. `DefaultMetadataList`
takes `config.listColumns ?? defaultColumns(config.primaryKey ??
'name')`. `defaultColumns` returns the primary key, `label` and
`description`, and each cell goes through `defaultCell`.
- **No registration gives `flow` or `hook` a `ListPage` or
`listColumns`.** `git grep registerMetadataResource` over `packages` and
`apps` gives 44 hits. The non-test registrations are
`builtinComponents.tsx` (object, field, permission, view, dashboard,
page, book), `anchors.ts#registerBuiltinAnchors`,
`datasource/register.ts` and `default-schemas.ts`, which sets
`defaultSchema` / `fieldOrder` only. `flow` and `hook` register only in
`anchors.ts`, with anchors, create fields and defaults. `git grep -E
'ListPage\s*:'` hits only `datasource/register.ts`, the positive
control. The shorthand spelling `ListPage[,}]` / `listColumns[,}]` has
no registration hit.
- `MetadataQuickFind` (`QuickFind.tsx`) indexes every type's items off
the same `client.list(type)` read. It keeps `label` and `description`
and draws `label` beside the name and `description` under it. It is
mounted on `DirectoryPage` and `StudioHomePage`.
- `MetadataClient.list(type)`
(`packages/data-objectstack/src/metadata-client.ts`) is `GET
{base}/{type}`, with base `/api/v1/meta`. It accepts a top-level array
or `items`.
- **Per-string control at objectui main `5d689c3`**, counted with `git
show REF:PATH | grep -cF` at both refs. Every cited string counts the
same at the pin and at main (1/1 each): the `listColumns ??
defaultColumns` line, the `description` and `label` default-column
entries, `if (customConfig?.ListPage) {`, the `:type` route element,
QuickFind's `label: item?.label,`, `description: item?.description,` and
`{r.description}`. The `ListPage:` and `listColumns` registration counts
also match.

## Premise, producer half: measured booted, not read

Instrument: a throwaway `@objectstack/verify` test in
`packages/qa/dogfood` (deleted after the run, never committed). It
booted the real `examples/app-showcase` composition in-process with
`bootStack(showcaseStack)`, signed in as the dev admin and read the
exact doors the list page reads. It was run twice, before and after a
container restart, with the same result.

| door | status | body | rows |
|:--|:--|:--|:--|
| `GET /api/v1/meta/flow` | 200 | top-level keys `type`, `items` | 30
flows. All 30 carry their authored `label` and `description`, with
`_packageId: com.example.showcase` |
| `GET /api/v1/meta/hook` | 200 | top-level keys `type`, `items` | 4
hooks. All 4 carry `label` and `description`, with `_packageId:
com.example.showcase` |
| `GET /api/v1/meta/package` | 200 | top-level keys `type`, `items` |
`com.example.showcase` with `scope: project`, which is what
`buildPackageScopeOptions` admits, so the list page's package scope
shows those rows |

The server-side list answer is
`packages/rest/src/meta-item-read-gate.ts#createMetaListAnswer`, which
both transports serve. Its type-specific steps are for `api`, `app`,
`view`, `doc` and `object` only. For `flow` and `hook` it runs the
per-caller gate and the translation step, and neither drops `label` or
`description`. Each row's `producer` cites that answer, the route, the
list-page fall-through, the registration and the client read.

## Verification, at `cb0206219c`

All heavy runs went through `scripts/pm/os-verify-lock.sh` with
`--maxWorkers=2`. The box is shared, so wall-clock figures are
shared-box readings.

- `pnpm --filter @objectstack/spec run check:liveness`: exit 0. "✓ every
governed-type property … is classified …" and "✓
packages/spec/liveness/state-counts.md is current".
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2` (the whole `local` project): **573 files, 16835 passed,
1 todo**.
- `@objectstack/spec` `repo` project, narrowed. The whole `repo` project
hit its 330 s timeout on the shared box, so that run is NOT MEASURED. It
was narrowed to the 10 `repo` files that read the ledgers:
`scripts/liveness/evidence.test.ts`,
`scripts/liveness/proof-registry.test.ts` and eight `*-retirement` /
`*.pin` tests. Result: **10 files, 211 passed**. CI runs the whole
project.
- `pnpm --filter @objectstack/spec typecheck`: exit 0. It covers `tsc
--noEmit`, `check:scripts-typecheck` and `check:test-typecheck`. `tsc -p
tsconfig.scripts.json --listFiles` names
`scripts/liveness/check-liveness.test.ts`, so the edited test is
compiled.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 68 commands at
`cb0206219c`. All 68 ran, and each exit code was captured before any
pipe. `--ran` over the exit-coded record reported: "✓ 68 derived
famil(ies) accounted for — 68 run, 0 NOT-MEASURED (a DERIVED zero …)".
  - 66 exited 0.
- `check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET:
eight unbuilt packages). It was re-measured after building them (all
turbo cache hits) and exited 0: "104 published require entry point(s)
across 66 package(s) load".
- `check:platform-checklist` exits 1 on a finding this diff does not
reach. See Acceptance notes.
- **Reverse verification** of the moved fixture, after the commit. The
fixture's carrier was swapped back to the flipped row through
`scripts/ablation-replace.mjs` (anchor `setEvidence(root, 'flow',
'active', ` hit ×1 → ×0, replacement ×0 → ×1, blob `9ff17ad4` →
`1091717a`). The case went **red**: `expected 1 to be +0`, and the gate
named `flow/description →
packages/plugins/driver-sql/src/sql-driver.ts`. That is the old fixture
failing because the row is now scanned as `live`. The tool restored the
file: blob after restore `9ff17ad4` equals `HEAD`, and `git diff HEAD`
is empty. The direction observed was a turn to red, as expected.
- Upstream check: `origin/main` has moved to `6427e2cf56` since the base
`fb386074f5`. None of the six paths changed upstream (`git diff
--name-only BASE origin/main -- PATHS` is empty), so the regenerated
counts need no merge.

## Acceptance notes

- **`check:platform-checklist` is red on the base and does not involve
this diff.** `docs/qa/platform-checklist/areas/identity-auth.json`
anchors `packages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor`.
Since `7d63088958` (PR objectstack-ai#20429), that symbol exists in the file only as a
member inside a `patch` object, which `symbol-anchors.mjs` does not
accept as a declaration. The files that finding names (the checklist
area, `auth-plugin.ts`, `scripts/check-platform-checklist.mjs`,
`scripts/symbol-anchors.mjs`, `scripts/checklist-select.mjs`) are
byte-identical between this branch's base `fb386074f5` and `origin/main`
`1378ec7c0c`, and none of the six paths here is among them. Carrier: the
plugin-auth / checklist owner. Not filed here.
- **Mentions left as they are, because the flip does not make them
false.** `docs/audits/2026-06-flowschema-property-liveness.md` records
the dated 2026-06 audit and is not a current-state claim.
`.claude/skills/spec-property-retirement/SKILL.md` §0 uses `hook.label`
/ `flow.description` as the example of "build the renderer, do not
retire", and that is what happened. `liveness/app.json`
(`areas.description`) and `liveness/job.json` cite "the hook.label
precedent" for "docs-shaped, kept, not warned", and that is still true.
- **Observation, objectui side (not filed).** `QuickFind.tsx`'s docblock
calls it a "Cmd+K palette", but it binds Cmd+Shift+M, to leave Cmd+K to
`CommandPalette`. The rows say "metadata quick-find" and name no key.
- Also noted: `hook` still has no registered metadata-admin preview. A
`HookPreview` is not needed for these rows, and objectui#11027 already
excludes it.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…the record key is the member's name (objectstack-ai#20300) (objectstack-ai#20458)

Fixes objectstack-ai#20300

Clause-②: no (narrowing)

Retires the inner `name` on analytics cube measures and dimensions
(`MetricSchema.name`, `DimensionSchema.name`). `measures` and
`dimensions` are records, and the record key was always the member's
identity: `GET /api/v1/analytics/meta` publishes every member as
`CUBE.KEY`, and every consumer resolves a member by indexing the bag
with its key. The inner copy was REQUIRED, read by nothing, and silently
ignored when it disagreed with its key.

ADR-0049 enforce-or-remove, by triage's verdict `5859547666` (RETIRE)
under the maintainer's criterion, verbatim: 「每族该问的是:主流平台有没有这个能力 —— 有 ⇒
补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读」. Cube.dev and LookML key a member by its
declared name, with no second inner name that can disagree.

**Tier H.** The diff touches `skills/objectstack-ui/rules/dashboards.md`
(a deletion only; see Deviations 1). It lands on the maintainer's word,
then the seat lands it. 103 files, +1446 / -423 (1869 changed lines,
under the 5,000 line class).

## Patch round after objectstack-ai#20390 (head `f639af5f3`)

The sections below describe `c4771e604`. This head adds two commits and
nothing else:

- **`5ce26b0b2`** merges `origin/main` `75b2169243` through
`os-regen-merge.sh`. The one hand conflict,
`packages/spec/vitest.repo-tests.json`, was resolved by stacking both
entries. Main’s step-18 siblings and this PR’s entries are all present
in the four registries.
- **`f639af5f3`** stamps `retiredAfter: 17.4.0` on
`cube-member-inner-name-removed`. objectstack-ai#20390 (`e956924e1`) made the stamp
required on every `retiredFromLoadPath: true` conversion. This entry is
unpublished, so it takes the package label, as `view-list-tabs-removed`
and `action-aria-removed` do on `main`.

The net diff is 103 files, +1447 / −423: the stamp is the one added
line. CI is green on this head. At-tier record `5875291969`: PASS.

## What this head carries (`c4771e604`)

| surface | change |
| --- | --- |
| schema | `retiredKey()` tombstones on `MetricSchema.name` and
`DimensionSchema.name` (both `strictObject`s, the `action.aria`
posture). `tsc` types the key `never`, and the parse raises the
prescription at `measures.KEY.name` / `dimensions.KEY.name`. The
`measures` / `dimensions` describes now state that the record key IS the
member's name. |
| D2 | `cube-member-inner-name-removed` (protocol 18,
`retiredFromLoadPath`), chained into `step18.conversionIds` with a
rationale paragraph. It strips the inner `name` from every member of
every `analyticsCubes[]` entry, and its notice names the cube. |
| D3 | semantic entry `cube-member-inner-name-retired`: the judgement a
DISAGREEING value still owes its author (which spelling was meant). |
| registration | `RETIRED_KEYS_BY_MAJOR[18]` gains `data/Metric:name`
and `data/Dimension:name` (per-file entries, generated region). |
| ledger | both `analytics_cube.json` rows STAY `dead` (the tombstone
keeps the key in the walked shape) with a `REMOVED 2026-09-28` note and
a re-measured `verifiedAt`. The README row is updated; the counts do not
move. |
| producers | `dataset-compiler.ts` stops writing it (the triage line),
and so do the two untyped internal mints tsc cannot see
(`CubeRegistry.inferFromObject`, and `inferCubeFromQuery` /
`inferMeasure` in `analytics-service.ts`). |
| authors | the showcase cube (8 members), the `service-analytics`
README example (3), and the published `objectstack-ui` skill example (6)
|
| fixtures | about 300 member literals and map-built members across 84
test and fixture files in eight packages; the three existing step-18
cube conversion fixtures are trimmed so the whole-table replay stays
disjoint |
| pins |
`packages/spec/src/data/cube-member-inner-name-retirement.test.ts`
covers every door (schema, `/meta` binding, `defineCube`, `defineStack`
with its `STACK_SCHEMA_INVALID`/422 envelope, and a `@ts-expect-error`
tsc leg), the D2 legs (stored row, boot door with a lit control, a
disagreeing value, idempotence, load-path retirement), the registration,
and a tree-scoped structural absence pin over the declared five-root
radius. The flipped `analytics.test.ts` blocks (the snake_case pins on a
value nothing read) are now tombstone pins. |
| changeset | `@objectstack/spec` minor (BREAKING banner, FROM → TO, the
one-line fix, what an author sees, and the ADR-0087 `registered`
marker); `@objectstack/service-analytics` patch |

What an author who still writes it sees: `tsc` fails at the authoring
site. The parse refuses it with: "`measures.METRIC.name` was removed in
@objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an
effect: the record key is the metric's name. … Delete the key. To rename
a metric, rename its key in `measures` — and every query, dashboard and
report that names `CUBE.KEY`. Run `os migrate meta --from 17` to list
the mechanical edits for existing sources; apply them by hand." A stored
or built cube heals at rehydration and at the artifact door.

## Zone 2, measured

- **A1 holds.** Zero reads of a member's inner `name` in non-test source
(`analytics-service.ts#getMeta` and `memory-analytics.ts#getMeta`
publish `CUBE.KEY`; `native-sql-strategy.ts#lookupMember` and
`memory-analytics.ts#resolveMeasure` / `#resolveDimension` index the bag
by key). Lit control: four reads of `measure.label` / `dimension.label`
in the same two projections. The one `measure.name` hit
(`dataset-compiler.ts:383`) is a `DatasetMeasure`, not a cube member.
objectui at pin `f8a9d0fb05`: no cube-member authoring. `CubeSchema` is
used only in `clientValidation.ts` (control: that hit resolves at the
same sha).
- **A2 holds, and is wider than the card.** Non-test producers:
`dataset-compiler.ts` (2 sites), `CubeRegistry.inferFromObject` (3) and
the ad-hoc mint in `analytics-service.ts` (4, plus `inferMeasure`'s 3
returns), the showcase, the README and the skill. Every one wrote the
name EQUAL to its key, so no producer writes a disagreeing value. Test
fixtures: 21 disagreed, all in `driver-memory` (e.g. `totalAmount: {
name: 'total_amount' }`), and every one was queried by its key
(`orders.totalAmount`). That is the trap, live in-repo. No
`platform-objects` or template authors any cube. Stored rows:
`analytics_cube` wraps as `analyticsCubes` at
`applyConversionsToStoredItem`, and the pin's stored-row leg replays it.
- **A3.** Worked on the merged cube contract (`public` enforced, objectstack-ai#20348
landed). Line numbers are from the merged tree.
- **A4.** Merged `origin/main` `6e3e5462c` (which carries objectstack-ai#20357's
step-18 appends) with `bash scripts/pm/os-regen-merge.sh`.
- The driverless merge-tree (a bare shared clone with no
`merge.os-regen.driver` registered) answered exit 0 with no conflicted
paths.
- The script's step 2 took main's side of
`content/docs/references/data/analytics.mdx`, which was regenerated from
the merged tree in its own commit (`e19628132`).
- After the merge: both sides' ids are present in both step-18 lists
(`view-list-tabs-removed` and `cube-member-inner-name-removed`) and in
the rationale.
- After the merge: `check:generated` reported all 15 artifacts current,
measured right after a spec build of the merged tree.

## Deviations

1. **The `skills/**` split was ordered, then withdrawn.** The seat
ordered the skill hunk split into a companion PR, and withdrew that on
this measurement:
- The example is an `os:check` block that `check:skill-examples`
type-checks inside `typecheck-consumers`, a member of the required
`TypeScript Type Check` aggregator.
- Putting one inner `name` back into the example with `ablation-replace`
(restored to the HEAD blob, `git diff HEAD` empty) gave exit 1:
`dashboards.md:450:15 error TS2322: Type 'string' is not assignable to
type 'undefined'`. So this PR without the hunk is red.
- A companion PR alone on `main` would be red too: at base `dbddf02c1`,
`MetricSchema.name` is a REQUIRED `z.string()`. That half is derived
from the schema, not built.
- No landing order is green, so the hunk stays here, as a pure deletion.
2. **File surface wider than the claim, same package and same defect
class.** `CubeRegistry.inferFromObject` and the `analytics-service.ts`
mints are untyped (a `Record` of `any`) producers that tsc cannot see;
A2 put every producer in scope. Fixture edits span `service-analytics`,
`driver-memory`, `spec`, `client`, `objectql`, `runtime`, `qa/dogfood`
and `qa/downstream-contract`.
3. **Route.** The `spec-property-retirement` skill's route table maps
`.strict()` to deletion plus a guidance map. I took the triage's
`retiredKey()` route instead, which `shared/retired-key.ts` documents
for closed shapes (strictly stronger than a guidance entry) and which
`action.aria` used this week. As a result the ledger rows stay, per the
card's acceptance. The `CubeJoinSchema` docblock line that said cube
shapes never take a tombstone is corrected.
4. **D2 strips a disagreeing value too.** Triage: "lossless when it
equals the key; a disagreeing value gets a D3 entry". The D3 entry
exists. The strip still removes a disagreeing value because the key
already won everywhere, so no answer changes, and leaving it would stop
the cube loading at the boot door. The notice prints both spellings
(`from: name "total_amount"`, `to: (removed; the record key
"totalAmount" is the name)`).
5. **`service-analytics` is graded `patch`.** Its members are filed
under the same keys, and every `/analytics/*` answer is unchanged.
`@objectstack/spec` carries `minor`: a published narrowing ships `minor`
in the launch window, and the changeset declares it as `Clause-②: no
(narrowing)` under its BREAKING banner.

## Tests (head `c4771e604` unless stated)

- **`@objectstack/spec`:**
- `test` 564 files / 16641 tests green (merged tree `e19628132`; spec
`src/` is unchanged since).
- `test:repo` 37 / 675 green (pre-merge `c1cae40df`). Post-merge, its
three tree-reading legs this diff owns were re-run green: the retirement
pin, `retired-key-migrate-sentence`, and `build-schemas-check-mode` (107
tests together with the pin).
- `typecheck` green (src, scripts, and the test layer under its
shrink-only ledger). `tsc -p tsconfig.test.json --listFilesOnly` lists
the new pin, so its `@ts-expect-error` legs are live.
- **`@objectstack/service-analytics`:** typecheck green, 132 files /
3093 tests green (`14cac89f4`).
- **`@objectstack/driver-memory`:** typecheck (which reaches the test
layer) green, 57 / 1374 green (`14cac89f4`). tsc found the two map-built
members there; the same shape was then swept in service-analytics and
runtime.
- **Touched test files in other packages:** `client` 7/7, `objectql`
`protocol-meta` 95/95, `runtime` `cross-field-refusal-operand-withhold`
11/11, `downstream-contract` `contract.test.ts` 13/13 plus typecheck
exit 0.
- **Reverse verification.** Putting `name: m.name` back in
`dataset-compiler.ts` via `ablation-replace` gave
`src/dataset-compiler.ts(673,7): error TS2322: Type 'string' is not
assignable to type 'undefined'` against the rebuilt spec `.d.ts`.
Restored: blob equals HEAD, `git diff HEAD` empty. The direction was the
predicted one (red).
- **Gates.** `dispatch-gates.mjs --commands` at `c4771e604` derives 126
families. All 126 were run and recorded, and `--ran` reports 0 UNRUN.
  - 123 exit 0.
- 2 exit 3, PREREQUISITE NOT MET: `check:dual-build-cjs-loads` and
`check:type-check-debt` (both need the whole-repo build).
- 1 exit 1: `check:platform-checklist`, inherited from `main` (see
Acceptance notes). Its inputs are byte-identical to `main`, and it is
not a per-PR CI gate.
- **Lint (a proven narrowing).**
- Scope: `eslint --no-inline-config --format json` over exactly the 95
changed code files returned 0 errors and 0 warnings.
- Population: read from `eslint.config.mjs` (`files:
['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`); the file count comes from
the JSON output.
- Invariance: the config itself records that it never enables type-aware
linting (no `parserOptions.project`), so no untouched file's verdict can
move.

**NOT MEASURED** (CI runs these):
- `qa/dogfood`: the two touched dogfood tests and
`expression-conformance`. The package does not resolve
`@objectstack/verify` unbuilt, so no test ran. Static reading: 0
`analytics.zod` references in that ledger, against 11 `.zod.ts`
references as the control.
- The showcase `typecheck`: 7 TS2307 for unbuilt connectors and plugins,
and 0 diagnostics in `showcase.cube.ts`.
- `downstream-contract`'s `consumer-specifier-ledger` needs
`@objectstack/cli` built.
- The two exit-3 gates above.

## Skills readings

`skills/objectstack-ui/rules/dashboards.md` goes 468 → 468 lines. The
whole package (every `SKILL.md`) goes 4404 → 4404. Against base the hunk
is 6 lines modified and nothing added: each change deletes a `name:
'KEY',` fragment.

## Acceptance notes (noted, not filed)

- `check:platform-checklist` is red on `main` at `3cf644938`:
- The failure: `areas/identity-auth.json` anchors
`plugin-auth/src/auth-plugin.ts#twoFactor`, and `7d6308895` (objectstack-ai#20429)
turned that line-start key into an inline nested object key (`plugins: {
twoFactor: true }`), which the shared resolver reads as absent by
design.
- It is independent of this diff: both files are byte-identical to
`main`.
- The gate is run by hand, not per PR. Carrier: the next PR to touch
`identity-auth.json` or `auth-plugin.ts`, or the checklist owner.
- The inner `name` carried a snake_case regex. The record key never had
one, and it legitimately takes camelCase and dotted spellings in-repo
(`driver-memory` fixtures; `'owner.amount_sum'` in
`dotted-measure-refusal.test.ts`). Nothing is enforced on the key today;
this is an observation, not a change here.
- The absence pin states its blind spot: members built under computed
keys (`Object.fromEntries(… { name: n, … })`). tsc found the typed ones
in `driver-memory`, and the rest of that shape was swept by an AST scan
(object literals holding `name`, `sql` and `type`). What remains is only
this pin's own refusal specimens and the schema shape.

## 维护者速读(草稿)

**改了什么**:分析立方体(cube)的度量与维度不再接受内部 `name` 字段;成员的名字就是它在 `measures` /
`dimensions` 里的键。写了 `name`
会在编译期和解析期被明确拒绝,并给出迁移提示。已存储的立方体在加载时自动去掉该字段,照常可用。

**为什么改**:这个字段从来没有任何代码读取,系统一律按键识别成员;当 `name`
与键不一致时,作者写的值被静默忽略。Cube.dev、LookML 等主流方案也只有一个名字。按您「主流平台有没有这个能力」的判据,判定退役。

**风险与代价(含回滚)**:对外行为不变 —— `/analytics/meta` 与查询接口的成员名仍是 `立方体.键`。仍写 `name`
的作者源码需要删除该字段(`os migrate meta --from 17` 列出改动)。回滚:还原本 PR 即可,无数据迁移需要撤销。本
PR 同时改了一个对外发布的 skill 示例(仅删除 `name`),因此需要您的批准。

**席位意见**:

**你要做的**:审阅后批准(Approve)本 PR。

Line 3 and Deviation 5 were amended by the `domain:spec` seat 1
(`session_01B3TqpoQbTAfG7G74GMDWNW`) before the at-tier review: this
diff widens no accept set and adds no export, so `Clause-②` is `no
(narrowing)`, as most retirements of this family on `main` declare.
`20323-action-aria-removed.md` declared `yes`; the definition in
`clause2-line.mjs` decides, not the precedent. The changeset line moved
with it in `2252e5728`, and the claim on objectstack-ai#20300 was amended in place.

The patch-round section above was added by the same seat after the
at-tier record `5875291969`.

---------

Co-authored-by: Claude <noreply@anthropic.com>
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

Development

Successfully merging this pull request may close these issues.

plugin-auth: one refused key in the auth settings pass drops every sibling setting, and says so only at warn

2 participants