Skip to content

feat(spec): a settings toggle can declare the question asked before it is turned off (confirmOffText) - #30489

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-4a9pd9
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-4a9pd9

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Requested by Yibo Wang · project thread

Fixes #30418
Ruling-ref: 6657805240

Clause-②: yes (widening: an optional confirmOffText on toggle specifiers; refused on every other specifier type)
Metadata-protocol: added
Skills-docs: none owed — a settings manifest is platform code that no app can declare, and no skills/** file teaches a specifier key (git grep -n -iE "specifier|settings.?manifest|email_password_enabled" -- skills/: two hits, skills/objectstack-platform/references/plugin-development.md:354, which names the feature_flags manifest and no key, and skills/objectstack-i18n/SKILL.md:193, which says settings is platform-only and stays true)
Prior rulings read: confirmofftext,confirmtext,settings manifest toggle → 0 hits; none; thread: 1 ruling(s) (6657805240); repo: objectstack-ai/objectstack
Liveness: not governed — the liveness ledger walks registered metadata kinds plus the unregistered kind map (authorableTypes() in check-liveness.mts); a settings manifest is neither, and no packages/spec/liveness/*.json carries a Specifier row, so there is no row to add. check:liveness green.
Carried: #30500, the domain:ui half (ruling (4)): the console's settings page asks confirmOffText before it saves a toggle turned off. Until it lands the key parses and is declared but is not shown.

维护者速读

  • 改了什么:设置清单的开关项(toggle)新增可选键 confirmOffText,即「关闭前要弹出的确认文字」;其他类型的设置项声明它会在解析时被拒绝。认证设置里的「启用邮箱/密码登录」开关声明了这句话(英、中、日、西四种语言)。
  • 为什么改:Setup 关闭邮箱/密码登录时不做任何提示,而用密码登录的人会因此需要别的登录方式;设置页是通用渲染器,此前没有声明式的办法让某个开关在关闭前先确认(维护者裁决 A,6657805240)。
  • 风险与回滚:只新增可选键,没声明的清单行为不变;目前控制台还不读这个键(属于 domain:ui 后半),所以本 PR 落地后界面暂时没有变化。回滚即还原本 PR。
  • 先看:packages/spec/src/system/settings-manifest.zod.ts 新键与 superRefine 新增的拒绝分支;packages/services/service-settings/src/manifests/auth.manifest.ts 的声明;settings-translation-coverage.test.ts 新增的覆盖要求。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/settings-manifest.test.ts,预期 78 例全绿,其中 Specifier.confirmOffText 一组 7 例。
  • 需要你做的:无,席位落地。

What changes

  • SpecifierSchema.confirmOffText (packages/spec/src/system/settings-manifest.zod.ts): I18nLabelSchema.optional(), described as "Confirm dialog text shown before a toggle's value changes to off (omit = no confirm)". Its TSDoc names the translation address and the toggle-only rule.
  • Toggle only: the specifier superRefine gains one arm beside the valueDomain-on-a-layout-specifier arm. On any type but toggle it adds a custom issue at ['confirmOffText']: "Specifier of type 'TYPE' has no off to confirm, so it must not declare a 'confirmOffText' (only a 'toggle' may)." It is an arm on the existing refinement, so no dropped-refinements row (the spec build is green).
  • Translation path (packages/spec/src/system/translation.zod.ts): the platform face's settings.NS.keys.KEY entry gains the leaf confirmOffText, beside the toggle's label, and the convention comment lists settings.NS.keys.KEY.confirmOffText. The keys entry is a strict object, so without this leaf two doors refuse a bundle carrying the text: PlatformTranslationDataSchema at parse, and tsc on the service's bundle literals, which are typed PlatformTranslationData. (The service's kernel:ready hook loads the bundle into the i18n service unparsed, settings-service-plugin.ts :296-302, so the runtime itself would not refuse it.) This is the translation half of "translated like the toggle's other text"; it sits in the platform-only settings group, which both app-authored doors already refuse.
  • auth.manifest.ts: email_password_enabled declares confirmOffText: 'People who sign in with a password will need another way in.' (the Setup and login page: turning off email and password asks for no confirmation, and the login page still draws the email form when it is off #30299 sentence), and the four shipped bundles carry it at settings.auth.keys.email_password_enabled.confirmOffText (en, zh-CN, ja-JP, es-ES).
  • settings-translation-coverage.test.ts: a declared confirmOffText is held to the same per-locale coverage as the toggle's label (key:KEY.confirmOffText is reported missing), and a bundle confirmOffText on a key that declares none is reported as undeclared copy.
  • Regenerated with the repo's generators: authorable-surface/system.json (system/Specifier:confirmOffText, written by the spec build), content/docs/references/system/settings-manifest.mdx and translation.mdx (gen:docs). check:llms-txt re-derived its 98 claims with no change (llms.txt counts schemas, not keys).
  • .changeset/30418-settings-toggle-confirm-off.md: @objectstack/spec minor (the protocol change), @objectstack/service-settings patch (a manifest declaration and its copy, graded as Authentication settings: turning off email and password with no other sign-in method saves without warning, and everyone is locked out #30202 graded its manifest change), with both declaration lines.

Tests

  • settings-manifest.test.ts, describe Specifier.confirmOffText — a toggle asks before it goes off: accepted on a toggle with its text; accepted as an inline locale map; absent still parses and reads undefined; refused on text and select (value-bearing) and group and action_button (layout), each with a control that the bare specifier parses, then exactly one custom issue at ['confirmOffText'] whose message names the type and the key.
  • translation.test.ts: the platform face accepts settings.auth.keys.email_password_enabled.confirmOffText with full safeParse success and keeps the value.
  • auth.manifest.test.ts: the parsed manifest's email_password_enabled is a toggle carrying the sentence, and the en bundle carries the same text at its address.
  • settings-translation-coverage.test.ts: the zh-CN / ja-JP / es-ES rows for settings.auth now also require the confirmOffText leaf (green with the three translations).

Acceptance notes

  • No resolveSettingsFieldConfirmOff helper is added to i18n-resolver.ts beside resolveSettingsActionConfirm: the console resolves settings copy through its own useSettingsLabel, and the ruling binds no resolver export. If the domain:ui half wants one, it is a small spec addition there.
  • The message is not pinned word for word; the pins assert the code, the path, and that it names the key and the type, which is what the ruling requires.

Verification

Measured at head 14289976ed (local subset; CI families are the PR's own check-runs). Head 1a29fff73a (review round 1) changes only the changeset grade and one test comment; on it translation.test.ts passes 218/218 and the Check Changeset family reruns green: check-changeset-no-major --base origin/main exit 0 ("This diff introduces no major bump."), check-empty-changeset --base origin/main exit 0, check-adr-0087-registration --base origin/main exit 0, pnpm check:changeset-gate-self-tests exit 0.

  • pnpm --filter @objectstack/spec test under os-verify-lock: VERDICT command-exit 0, Test Files 815 passed (815), Tests 25626 passed, 1 todo.
  • pnpm --filter @objectstack/service-settings test && … typecheck under os-verify-lock (closure built first): VERDICT command-exit 0, Test Files 68 passed (68), Tests 1157 passed; tsc --noEmit clean.
  • pnpm --filter @objectstack/spec check:generated after gen:docs: every artifact current (the first run named only content/docs/references/** stale, which gen:docs regenerated).
  • Ablation, one leg (commit first, then mutate): node scripts/ablation-replace.mjs replaced the anchor if (spec.confirmOffText !== undefined && spec.type !== 'toggle') { with if (false) { (anchor 1 → 0, blob cc1a18591108 → 4425afbb53ce) and ran vitest run src/system/settings-manifest.test.ts: Tests 4 failed, 74 passed — exactly the four refusal pins (text, select, group, action_button). Restored: blob == HEAD and git diff HEAD empty. The spec test imports the schema from source, so no dist leg is needed.
  • node scripts/pm/dispatch-gates.mjs --ran FILE --repo objectstack-ai/objectstack: "106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN". Four families first exited 3 (prerequisite: a shallow clone for check-plugin-teardown-shape --self-test; built dists for check:skill-examples, check:dual-build-cjs-loads, check:i18n-walk-parity); after git fetch --unshallow origin main and a full turbo run build --concurrency=2 (VERDICT command-exit 0) all four exit 0. check-changeset-no-major: "This diff introduces no major bump."
  • Local-only: pnpm check:dev-prereqs exit 0; pnpm check:skill-frame-freshness exit 0.
  • node scripts/pm/check-prior-rulings.mjs --card 30418 --terms …: exit 0, line above.
Gate record (106 lines, as reconciled)
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:api-surface :: exit 0
pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0
pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0
pnpm --filter @objectstack/spec run check:export-origins :: exit 0
pnpm --filter @objectstack/spec run check:exported-any :: exit 0
pnpm --filter @objectstack/spec run check:generated :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:llms-txt :: exit 0
pnpm --filter @objectstack/spec run check:skill-refs :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:package-naming :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:spec-docblock-symbol-anchors :: exit 0
pnpm check:spec-parsed-alias :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:i18n-walk-parity :: exit 0

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzhstcrHvzYEWDZZDBx

…t is turned off (confirmOffText)

SpecifierSchema gains an optional confirmOffText (I18nLabel), refused on every specifier type but toggle. The platform translation bundle carries it at settings.NS.keys.KEY.confirmOffText, and the auth manifest's email_password_enabled toggle declares the sentence from the card, in all four shipped locales.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzhstcrHvzYEWDZZDBx
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:system tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 172 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 48231b024ed7a9f0c9be73f34a5bf5f48c7a1d68 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8973d2523b6986b5efb6c5e4a6a6240b7dead709 — the merge of head 1a29fff73ae896337bf1a00f98f8e33e4ca77685 into base 48231b024ed7a9f0c9be73f34a5bf5f48c7a1d68, 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 8973d2523b6986b5efb6c5e4a6a6240b7dead709 && git checkout 8973d2523b6986b5efb6c5e4a6a6240b7dead709
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 48231b024ed7a9f0c9be73f34a5bf5f48c7a1d68 1a29fff73ae896337bf1a00f98f8e33e4ca77685 && git checkout -B drift-repro 48231b024ed7a9f0c9be73f34a5bf5f48c7a1d68 && git merge --no-ff 1a29fff73ae896337bf1a00f98f8e33e4ca77685

node scripts/docs-audit/affected-docs.mjs --json 48231b024ed7a9f0c9be73f34a5bf5f48c7a1d68

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 1428997 · 2026-11-05T13:24Z

Full review of the first head at CONTRACT_REVIEW_TIER (a new authorable key: confirmOffText on settings-manifest toggle specifiers, refused on other types, executing ruling A 6657805240).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 14289976eda6a3c6b9778258bec2e1b1e8175435
Local-runs: none
Scope: the PR diff against merge-base 989ae42d01 (all 15 files, 140 insertions, 6 deletions, one commit); packages/spec/src/system/settings-manifest.zod.ts at head (SpecifierHandlerSchema.confirmText :97 and :103, SpecifierSchema :389, the new key :536-547, the superRefine :555, the valueDomain arm :575-581, the new arm :586-592); packages/spec/src/system/settings-manifest.test.ts :306-344; packages/spec/src/system/translation.zod.ts (convention comment :2274-2288, platformSettingsShape :2269, the strict keys entry :2312-2324, actions aliases :2331, TranslationDataSchema :2365-2380 and the platform-only note :2241-2258); packages/spec/src/system/translation.test.ts :1800-1847; packages/spec/src/shared/strict-object.ts :541-543; packages/spec/src/system/i18n-resolver.ts :5228-5260; packages/services/service-settings/src/manifests/auth.manifest.ts :26-36; auth.manifest.test.ts :1-8 and :291-302; service-settings/src/translations/{en,zh-CN,ja-JP,es-ES}.ts and index.ts :28; settings-translation-coverage.test.ts at head (:52-56, :82-90, :108-135, :150-164); settings-service.ts :1233-1250 and :2860-2870; settings-service-plugin.ts :296-302; ui/apps/console/src/pages/settings/useSettingsLabel.ts :12-14, :100, :151-157 and SettingsField.tsx :625; packages/spec/liveness/README.md, packages/spec/scripts/liveness/check-liveness.mts :1443-1453, packages/spec/src/kernel/metadata-type-schemas.ts :255; packages/spec/{api-surface,declaration-map,json-schema.manifest}/system.json, dropped-refinements.baseline.json, authorable-surface.base.json, llms.txt (grep census); scripts/check-changeset-no-major.mjs :1-40 and :2180-2200; scripts/pm/clause2-line.mjs :443 and :558; scripts/check-i18n-walk-parity.mjs :195-232; .changeset/30202-keep-a-sign-in-method.md :1-5; card #30418 body and 5 comments (triage 6656303889, ruling 6657805240, two claims, os-dev-report 6659112911); PR #30489 body, 2 comments, 0 reviews; check-runs on the head.

① Derived judgments

  1. Ruling bind (1), the key. settings-manifest.zod.ts :545-547 declares confirmOffText: I18nLabelSchema.optional().describe("Confirm dialog text shown before a toggle's value changes to off (omit = no confirm)"), the ruling's sentence with its initial capitalised, on SpecifierSchema (:389) beside handler (:534) and ahead of childNamespace (:550). It uses the same I18nLabelSchema import (:7) the handler's confirmText uses at :97 and :103, so a bare string and an inline locale map both parse, which the pins at :310-318 cover. The TSDoc at :536-544 names the translation address and the toggle-only rule. Nothing like confirm: { when, text } is added anywhere in the diff (bind (5)); the Authentication settings: turning off email and password with no other sign-in method saves without warning, and everyone is locked out #30202 server floor is untouched (no plugin-auth file in the diff).
  2. Ruling bind (2), toggle only. The arm at :586-592 sits inside the existing specifier superRefine (:555), directly after the valueDomain arm (:575-581) and before the select/radio/multiselect options arm (:593), as the order asks. Predicate spec.confirmOffText !== undefined && spec.type !== 'toggle' refuses every non-toggle type, input and layout alike; issue code: custom, path: ['confirmOffText'], message Specifier of type '${spec.type}' has no off to confirm, so it must not declare a 'confirmOffText' (only a 'toggle' may). names both the key and the type. One honest boundary, shared with the valueDomain arm it mirrors: SettingsService.registerManifest (:1233) takes a SettingsManifest without parsing it through SettingsManifestSchema (no .parse or safeParse of a manifest anywhere in settings-service.ts), so the refusal bites at the spec parse door and in each manifest's own test (auth.manifest.test.ts :296 parses), not at boot. That is the standing shape of every arm in this superRefine, not something this PR changes.
  3. Ruling bind (3), the declaration. auth.manifest.ts :35 declares confirmOffText: 'People who sign in with a password will need another way in.' on the email_password_enabled toggle (:27-28), the Setup and login page: turning off email and password asks for no confirmation, and the login page still draws the email form when it is off #30299 sentence as a sentence. The translation path is the toggle's own: the platform bundle address settings.auth.keys.email_password_enabled.confirmOffText sits beside .label in en.ts :175-178, zh-CN.ts :285-288, ja-JP.ts :138-141, es-ES.ts :138-141, the same settings.<ns>.keys.<key>.* convention the console's useSettingsLabel probes for label, help and placeholder (:151-153) and resolveSettings* reads on the server. The console probe for this leaf and any resolveSettingsFieldConfirmOff export are the domain:ui half (ruling (4)); the PR's acceptance note says so and the Carried: line names it. The four bundles carry the text; SettingsField.tsx :625 (case 'toggle') is untouched, so nothing shows yet, as the body states.
  4. The flagged deviation, translation.zod.ts. Necessary: the keys entry at :2312 is a strictObject (strict-object.ts :541-543 builds a .strict() closed object whose unknown-key refusal is terminal), so { label, confirmOffText } was refused by PlatformTranslationDataSchema before this PR, and settingsBuiltinTranslations is typed PlatformTranslationBundle (translations/index.ts :28), so the four bundles would not have typechecked either. Correctly scoped: the leaf is added only to the platform face's keys entry (:2322-2323), the one address where a specifier's labels live; the actions entry already has its own confirmText (:2338) and is not touched; the per-app TranslationDataSchema (:2365-2380) refuses the whole settings group by name, pinned at translation.test.ts :1803-1830, so no app-authored door widens. Ripples: the generated translation.mdx row moved (the { … } truncation at :344 and :512), check:i18n-walk-parity does not walk settings on the per-app face (it left that face on [finding] TranslationDataSchema.settings is declared, read by the settings service, and extractable by nothing — registry-driven emitter, or removal from the per-app schema (ADR-0049) #15178, :200-209, and the ledger at :215-224 names only messages and settingsCommon), no scripts/check-i18n-* gate names settings, and llms.txt carries no Specifier or confirmText token at all (0 hits), so its claims are unmoved, as the body says. Locales: the settings group is carried by service-settings alone (no settings group in ui/packages/i18n/src/locales or platform-objects translations; email_password_enabled appears in no other bundle), and that package ships four locales, not eleven; the coverage test holds zh-CN, ja-JP and es-ES to full coverage of a declared confirmOffText (:85-88) and refuses undeclared copy in all four (:128-132, :159-164), while a runtime miss falls back to the manifest literal (useSettingsLabel :100 asks i18n.exists before translating and returns the fallback). Fall back, not refused, which is the repo's rule for served translations, and the shipped locales are held complete by the test rather than by a gate. One precision on the body's wording: nothing parses the bundle at boot (settings-service-plugin.ts :296-302 hands it to i18n.loadTranslations raw), so "the bundle the service ships would be refused" is true of the contract and of tsc, not of a running server. Judged accurate as a contract statement.
  5. Ripples by generator. authorable-surface/system.json :1139 gains the one row system/Specifier:confirmOffText in sorted position (the spec build writes it; check:authorable-surface reported green); authorable-surface.base.json is the deletion anchor rewritten only by --update-base (build-schemas.ts :191-195) and correctly stays put. settings-manifest.mdx gains the row twice (:126 and :209, the two tables that print Specifier), and translation.mdx twice, in gen:docs shape. api-surface/system.json, declaration-map/system.json, json-schema.manifest/system.json and llms.txt list exports and schemas, not keys (0 confirmText hits in each at head), so they rightly do not move. dropped-refinements.baseline.json has no settings-manifest site and an arm on an existing superRefine adds none (check:generated green per the dev). No file in the diff is hand-edited generated output by reading.
  6. Liveness. packages/spec/liveness/README.md roots the walk in BUILTIN_METADATA_TYPE_SCHEMAS and sets the denominator as registered kinds plus listUnregisteredKindSchemaTypes() (check-liveness.mts :1443-1453); UNREGISTERED_KIND_SCHEMAS (metadata-type-schemas.ts :255) holds webhook, connector, analytics_cube and kin, no settings manifest; packages/spec/liveness/ has no settings_manifest.json and no Specifier row in any ledger. Not governed; the body says so, as the order's item 5 allows. Spec property liveness is green on the head.
  7. Declarations. PR body and changeset both carry Clause-②: yes (widening: …) (arm read as widening by readClause2Line, clause2-line.mjs :443) and Metadata-protocol: added (readMetadataProtocolLine :558). The changeset grades @objectstack/spec minor, which is what added owes (check-changeset-no-major.mjs :9-15, :2187-2188), and moves the authorable-surface/ baseline, consistent with a declared kind; Check Changeset is green and the dev's local run read "no major". @objectstack/service-settings is also graded minor; the gate accepts it because the predicate is PR-scoped (:17-20), but the LEVEL axis says a minor is earned by a protocol change and by nothing else (:2191-2193), and this package's change is a manifest declaration plus bundle text, not protocol; the nearest precedent, .changeset/30202-keep-a-sign-in-method.md :1-4, graded the same package patch. A should-fix, not a gate failure.
  8. Pins. settings-manifest.test.ts :306-344: acceptance on a toggle with the text kept (:309-312), an inline locale map (:314-317), absence parsing to undefined (:319-322), and refusal on four types, text and select (input) and group and action_button (layout), each with a control that the bare specifier parses, then exactly one custom issue at ['confirmOffText'] whose message contains the type and the key (:326-343). That exceeds the order's "one input type, one layout type". The ablation reported (arm to if (false), 4 red of 78, restored blob equal to HEAD) matches by reading: nothing else in the file exercises the arm. translation.test.ts :1838-1846 pins the platform face accepting the leaf with safeParse success and the value read back. auth.manifest.test.ts :295-302 parses the shipped manifest, asserts the specifier is a toggle carrying the exact sentence, and that the en bundle carries the same string at its address. The coverage test's new paths (:85-88, :128-132) run on every shipped locale and are green only because all three translations exist. All real.
  9. Body matches code: every file the body names is in the diff and nothing in the diff is unnamed; the message quoted in the body is byte-equal to :590; Fixes #30418, Ruling-ref: 6657805240, Skills-docs:, Prior rulings read:, the liveness answer, Carried: naming the domain:ui half and the six-line ## 维护者速读 are all present. Scope sits inside the order: the only file beyond the claim's named surface is translation.zod.ts, flagged in the report's deviations and in the body. 140 changed lines, no governed surface in the file list, Governed Surface Queue Guard green. Required contexts Build Core, Test Core (8 shards), Type Check and Lint were still in_progress at review time.

② Semver level

@objectstack/spec minor: a new authorable key on a settings-manifest shape (Metadata-protocol: added), refused only where it was never accepted before (no specifier parsed confirmOffText before this PR, as the strict-free z.object would have stripped it; the arm turns a silent strip into a named refusal on non-toggles, the Prime Directive #10 direction). Clause-②: yes (widening) is the right pair. @objectstack/service-settings patch is what the LEVEL axis owes; minor is tolerated by the gate but over-graded.

③ Boundary flags

  • The arm's spec.type !== 'toggle' is broader than the valueDomain arm's !SPECIFIERS_REQUIRING_KEY.has(spec.type), by design: the ruling's "any type other than toggle" means a checkbox-less, value-bearing text is refused too, and the pins cover it. No specifier type beside toggle has an off, so nothing is over-refused.
  • The refusal is a parse-time door; the service registers manifests unparsed (:1233), so a misplaced key in a platform manifest is caught by that manifest's test, not at boot. Same standing as every other arm in this superRefine.
  • The console still ignores the key (SettingsField.tsx :625 toggle arm untouched), and useSettingsLabel has no fieldConfirmOff probe; both are the carried domain:ui half. Until it lands the declaration is parsed, translated and not shown, which the body and changeset say in those words.
  • The bundle is loaded raw at runtime (settings-service-plugin.ts :296-302); the strict leaf governs the contract and tsc, not a server's boot.

④ Design judgment

The fix is at the right layer and in the ruled shape: one optional key on SpecifierSchema named after the existing handler confirmText, one arm on the existing refinement beside the arm it mirrors, the translation leaf at the toggle's own address on the platform face only, and the manifest declaration with the ruled sentence carried in all four shipped locales and held there by the coverage test. Should-fix, in order: (1) regrade @objectstack/service-settings to patch in .changeset/30418-settings-toggle-confirm-off.md :3, the level a non-protocol change owes under the LEVEL axis and the #30202 precedent; (2) add confirmOff: 'confirmOffText' and confirm: 'confirmOffText' to the keys entry's aliases at translation.zod.ts :2315, mirroring confirm: 'confirmText' on the actions entry (:2331), so a borrowed spelling is named rather than sent round again; (3) soften the body's "without this leaf the bundle the service ships would be refused" to name the two doors that refuse it (PlatformTranslationDataSchema and tsc on the typed bundle), since the running service loads the bundle unparsed. None blocks landing; landing waits on the still-running required contexts going green.

Implemented-by: claude/project-thread-4a9pd9
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

VERDICT: PASS

…eset, and name the doors that refuse a bundle without the leaf

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzhstcrHvzYEWDZZDBx
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

维护者速读

席位意见:fable 契约复审 PASS,没有必须修的。五条裁决约束都核对过。开发者自报的偏差(translation.zod.ts 加一个叶子,让带这句翻译的语言包能通过解析)判定为必要,范围也对。采纳了两条建议:service-settings 的版本级别改成 patch,正文写明实际拒绝它的两道门。没采纳给新键加别名,因为新键没有旧拼写。

席位核对 — ACCEPT · domain:spec#3 · head 1a29fff73ae896337bf1a00f98f8e33e4ca77685

  • Shape: 15 files (+141 −6) on merge-base 989ae42d:
  • Premise measured: at merge-base, only action_button handlers could declare a confirmation (confirmText, settings-manifest.zod.ts :97, :103); toggles had none.
  • Gates: at 14289976, spec 815/815 files and service-settings 68/68 green; dispatch-gates --ran 106/106 with 0 NOT-MEASURED; the ablation turns the 4 refusal pins red (dev report 6659112911). At 1a29fff7, check-changeset-no-major, check-empty-changeset, check-adr-0087-registration and the changeset-gate self-tests exit 0, and translation.test.ts is 218/218. Required check-runs on this head were still running at ACCEPT time; auto-merge waits for them.
  • Contract review: full review 6659287829 on 14289976: PASS, no must-fix. It checked all five binds of ruling 6657805240, judged the translation.zod.ts leaf necessary (the keys entry is a terminal strict object and the bundle is typed PlatformTranslationBundle) and scoped to the platform face, found no 11-locale gate over settings, and confirmed the pins are real. The seat read the round-1 delta (2 files, +3 −2): the service-settings changeset level, and the corrected pin comment. Should-fix 2 (aliases on the keys entry) is declined: a new key has no legacy spelling to alias.
  • Declarations: Clause-②: yes (widening: …), Metadata-protocol: added, Ruling-ref: 6657805240. ADR-0087 not needed.
  • Accepted deviations: the translation.zod.ts leaf, as above.
  • Carried: console: the settings page asks a toggle's confirmOffText before it saves the toggle turned off (ruling A on #30418, the domain:ui half) #30500 (p1, domain:ui, Blocked-by Settings manifest: a toggle cannot declare a confirmation before it is turned off, so Setup turns off email and password sign-in without asking #30418): the console's settings page asks confirmOffText before it saves a toggle turned off.
  • Landing: not governed (check-governed-merges --pr 30489: 147 lines). The hand-off is pr_ready plus automerge_enable, through the merge queue.

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 protocol:system size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Settings manifest: a toggle cannot declare a confirmation before it is turned off, so Setup turns off email and password sign-in without asking

1 participant