Skip to content

fix(platform-objects): Setup Create User marks Email required while phone sign-in is off - #28707

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-5fg7d3
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-5fg7d3

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Requested by Leehom · project thread

Fixes #28280

Clause-②: yes (widening)
Metadata-protocol: semantic

维护者速读

  • 改了什么:Setup → 用户 → 创建用户:未开启手机号登录(默认)时,「邮箱」在提交前就带必填星标,留空会在对话框里直接拦下;开启手机号登录时,邮箱仍是可选,旁边是手机号。控制台还没拿到 /auth/config 的应答时(加载中或请求失败),邮箱不带标记,和 main 上一样。
  • 为什么改:服务器在未开启手机号登录时拒收空邮箱(400 "Email is required"),但对话框把邮箱画成可选,管理员只有提交后才知道。fix(console): Create User marks a missing email on its input, in the user's language #28649 已能在提交后标出服务器点名的字段,这张卡补上提交前的标记。
  • 风险与回滚:中低。sys_user.create_user 的邮箱参数拆成两个(同一字段,按 features.phoneNumber 各自显隐,对话框只画并只提交显示的那个)。为此动作门的参数校验 validateActionParams 改了一处语义:同名参数声明多次时,只有每一处都必填才算必填,值形状按第一处(以前是最后一处说了算,会让 REST 动作门把只填手机号的创建拒成 400)。仓库、示例和 hotcrm 里没有别的动作重复声明参数名,所以别处行为不变。外加 public-auth-features.ts 登记一条路径。纯 revert 即可回滚。
  • 先看:packages/spec/src/ui/action-params.zod.ts(同名合并那一行);packages/platform-objects/src/identity/sys-user.object.ts(两个邮箱参数的顺序与 visible);packages/runtime/src/domains/api-action-create-user-email-param.test.ts(经真实 REST 动作门的三条路径)。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-create-user-email-param.test.ts,预期 5 例全绿(只填手机号的创建能到达接口;空邮箱由接口自己答 400 "Email is required")。
  • 需要你做的:无,席位落地(含 packages/spec 路径的 Contract review)。

What changes

  • sys_user.create_user declares two params on the one email field (packages/platform-objects/src/identity/sys-user.object.ts). A param's required is a static boolean, and the endpoint requires the email only while phone sign-in is off, so:
    • first { field: 'email', required: false, requiresFeature: 'phoneNumber' }, which lowers to features.phoneNumber == true;
    • then { field: 'email', required: true, visible: 'has(features.phoneNumber) && features.phoneNumber != true' }.
      The console collects and submits only the params whose gate says yes (filterVisibleParams), so exactly one Email input renders. The rendered order is unchanged: Email, then Phone Number when it shows, then Name.
  • The action doors judge a twice-declared name by every declaration (packages/spec/src/ui/action-params.zod.ts, under the one-PR exception the seat extended to this file). validateActionParams, which enforceActionParams runs on the REST action route and the MCP run_action bridge before the handler, keyed params by name with the LAST declaration winning, so the required second Email param made the REST door refuse a phone-only create (400 VALIDATION_ERROR "Action param "email" is required"). A name declared more than once is now required only when EVERY declaration is, and its value shape is the FIRST declaration's; unknown-key handling is unchanged. The door reads no visible or features, so the endpoint stays the judge of a blank email.
  • The registry (packages/spec/src/kernel/public-auth-features.ts): PUBLIC_AUTH_FEATURES.phoneNumber.gatedInputs books sys_user.actions.create_user.params.email. The row's notes say the path stands for two params, the second negatively gated, and the module and gatedInputs docblocks say a param path resolves to the first param on its field and the gate is checked as a conjunct. No registry model change and no required-when predicate, as ruled.
  • The guard's header (packages/platform-objects/src/feature-gate-guard.test.ts) states that this one booked path stands for two params, one of them negatively gated. The guard's logic is untouched.
  • Tests: a platform-objects pin that evaluates the declared gates through the canonical CEL engine under the console's show rule; an app-shell test that opens Create User through the console runtime with the served features; a runtime test that drives the real REST action route with the real SysUser declaration; two validateActionParams cases in action-params.test.ts.
  • Changeset .changeset/28280-create-user-email-required-mark.md: @objectstack/spec minor (the semantic change to a twice-declared param's required at the doors) and @objectstack/platform-objects patch. Both packages ship dist, and the built dist of each carries the change.

Tests

Red first on origin/main 49a6a5c, green at head

pnpm --filter @objectstack/platform-objects exec vitest run src/identity/sys-user-create-user-email-required.test.ts, with only the test file on top of origin/main:

× phone sign-in off: one Email input, and it is required
× the first Email param carries the canonical phoneNumber gate the feature-gate guard resolves
Tests  2 failed | 2 passed (4)
os-verify-lock: VERDICT command-exit 1

At head, with the guard file (after pnpm --filter @objectstack/spec build):

✓ forward … sys_user.actions.create_user.params.email is gated on phoneNumber
✓ reverse … sys_user.actions.create_user.params.email references registered flag phoneNumber and is booked  (x3)
✓ 4/4 of the new pin
Tests  116 passed (116)

Whole package: pnpm --filter @objectstack/platform-objects test → Test Files 107 passed (107) · Tests 1496 passed (1496).

The Create User dialog (app-shell)

useConsoleActionRuntime.createUserEmailRequiredMark-28280.test.tsx boots the dialog the way #28649's test does (real ActionRunner with the console runtime's api and param handlers, real ActionParamDialog, real Chinese I18nProvider), inside the console's own ExpressionProvider fed the served features, with sys_user's fields so the field-backed params resolve through resolveActionParams:

  • phone sign-in off: one Email input, aria-required="true", no Phone Number; a blank Confirm shows 邮箱 为必填项, marks the input invalid, and sends no request;
  • phone sign-in on: one Email input, not required, Phone Number shown; a blank Confirm goes to the server;
  • no served flag (features is {}): one Email input, not required;
  • control: with phone sign-in off a typed email submits once.

cd ui && pnpm exec vitest run packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.createUserEmailRequiredMark-28280.test.tsx packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.createUserRequiredField-28289.test.tsx → Test Files 2 passed (2) · Tests 10 passed (10) (#28649's six server-refusal marks stay green).

The REST action door (runtime), red first and the merge ablated

pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-create-user-email-param.test.ts, driving HttpDispatcher.handleActions('/sys_user/create_user', 'POST', …) over the api-action-forward.test.ts recording target, with the real SysUser from @objectstack/platform-objects/identity:

  • the declaration carries two email params, [false, true];
  • phone-only { phoneNumber, name } passes the door and is forwarded to /api/v1/auth/admin/create-user;
  • a blank email passes the door; the caller gets the ENDPOINT's 400 VALIDATION_FAILED "Email is required" with its fields, relayed whole (the door does not refuse it);
  • an email is forwarded with it;
  • control: a non-string email is still refused at the door (value shape from the first declaration).

Red first, with packages/spec/src/ui/action-params.zod.ts at 2310b30 and spec rebuilt: × phone-only … reaches the endpoint with {"code":"VALIDATION_ERROR","message":"Invalid action params: Action param \"email\" is required","httpStatus":400}, × blank email … (the seat's finding). That run was on a six-case draft: 3 failed / 3 passed, the third an MCP case that failed on the system-object exposure refusal instead and was dropped (see below). Spec unit cases: 2 failed / 33 passed.

Merge ablated through scripts/ablation-replace.mjs (declared.set(p.name, p), last wins), spec rebuilt, dist hits of the merge 0: door test 2 failed / 3 passed, spec cases 2 failed / 33 passed. Restored to the HEAD blob (git diff HEAD empty), rebuilt, dist hits 1: 5/5 and 35/35.

The MCP bridge is not driven for this action: create_user declares no ai.exposed, so run_action refuses it as a system-object action before any param check (measured). Both doors call the same enforceActionParams.

Ablations (each through scripts/ablation-replace.mjs, restored to the HEAD blob, git diff HEAD empty)
Leg Mutation Reading
A1 source: the required Email param made required: false × phone sign-in off: one Email input, and it is required · 1 failed / 115 passed
A2 source: has(features.phoneNumber) && dropped (bare features.phoneNumber != true) × no served flag …: one Email input, optional · 1 failed / 114 passed
A3 source: the two Email params swapped (required first) × the first Email param carries the canonical phoneNumber gate … and the guard's forward row × sys_user.actions.create_user.params.email is gated on phoneNumber · 2 failed / 114 passed
U1 ui mirror: the required param made optional × phone sign-in off: … required mark … · 1 failed / 3 passed
U2 ui mirror: bare features.phoneNumber != true × no served flag … · 1 failed / 3 passed

The platform-objects pin imports the object from src/ (relative import), and ui vitest aliases @object-ui/* to src/, so no leg needed a rebuild. A3 confirms the order matters: the forward guard resolves the path to the first param on the field.

Acceptance notes

  • One reading beyond the order's literal. The order spelled the required param's gate features.phoneNumber != true. Measured (A2, U2): while the console holds no served flag (its features is {} until /auth/config answers, and stays {} if that fetch fails) both gates fault on the absent key, and the dialog shows a param whose gate faults, so two Email inputs render. The has(features.phoneNumber) && term keeps the required param hidden then, and Email stays optional as it was. The precedent spelling is sys-user.page.ts's has(features.twoFactor) && features.twoFactor == true. The first, optional param keeps the canonical gate the guard wants.
  • The endpoint is unchanged: it still refuses a blank email with phone sign-in off; no server file is in this diff.
  • Census for the semantic change: an AST scan of every params / actionParams array literal in this repository (430 arrays, ui/ and examples/ included) and in hotcrm origin/main (7 arrays), plus the 2 JSON params arrays, finds no name declared twice except create_user's email. No existing action relied on last-wins.
  • The reverse guard has three email rows (one per features.phoneNumber reference across the two params), all booked by the one path.
  • The reverse guard's red without the booking was measured on the card (2 failed) and is not re-run here.

Verification

Local gates at head

Local gate run at head 9684fb3, derived by dispatch-gates --commands (87 families), each exit captured before any pipe.
Re-run: pnpm --dir ui exec vitest run scripts/ first exit 1 (unit-registry-absence-collision could not import app-shell modules while @objectstack/client dist was still being built in this fresh worktree), then exit 0 (169 files) once it was built.

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-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-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-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.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 --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: 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: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:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:engine-double-contract :: exit 0
pnpm check:error-code-casing :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-stale-fill :: 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:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: 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:route-envelope :: 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:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

dispatch-gates --ran: 87 derived, 86 run, 1 NOT-MEASURED (check:dual-build-cjs-loads, PREREQUISITE NOT MET: needs every package dist), 0 UNRUN.

Also: pnpm --filter @objectstack/spec test :: exit 0 (783 files, 23175 tests); pnpm --filter @objectstack/spec typecheck :: exit 0; pnpm --filter @objectstack/runtime test :: exit 0 (505 files, 8191 tests); pnpm --filter @objectstack/runtime typecheck :: exit 0; pnpm --filter @objectstack/platform-objects test :: exit 0 (107 files, 1496 tests); eslint --no-inline-config on the touched spec, runtime and platform-objects paths :: exit 0. The app-shell test and type-check are unchanged since 2310b302 (green there).

Generated by Claude Code

…n-in (red first)

Co-Authored-By: Claude <noreply@anthropic.com>
…ign-in is off

Two params on the one email field: the optional, phoneNumber-gated one
first, then a required one shown while the served config says phone
sign-in is off. Book the path in phoneNumber.gatedInputs.

Co-Authored-By: Claude <noreply@anthropic.com>
…hile phone sign-in is off

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 4 documentable anchor(s).

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

  • content/docs/permissions/authentication.mdx (via phoneNumber (symbol, a field of const object PUBLIC_AUTH_FEATURES), phoneNumber (literal, a string literal in actions))
  • content/docs/protocol/objectui/actions.mdx (via phoneNumber (symbol, a field of const object PUBLIC_AUTH_FEATURES), phoneNumber (literal, a string literal in actions))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via phoneNumber (symbol, a field of const object PUBLIC_AUTH_FEATURES), phoneNumber (literal, a string literal in actions))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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 — 168 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 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0fb8c97c40e475b1024f94a88db414be819336ad — the merge of head 9684fb3ba196ca4e8fd9a31ed3bffe40cac30ba8 into base 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8, 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 0fb8c97c40e475b1024f94a88db414be819336ad && git checkout 0fb8c97c40e475b1024f94a88db414be819336ad
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8 9684fb3ba196ca4e8fd9a31ed3bffe40cac30ba8 && git checkout -B drift-repro 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8 && git merge --no-ff 9684fb3ba196ca4e8fd9a31ed3bffe40cac30ba8

node scripts/docs-audit/affected-docs.mjs --json 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8

⚠️ 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 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8 → 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

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2310b3028baa8116a7c14c01be43b2a10d8319ce
Local-runs: none
Scope: packages/spec/src/kernel/public-auth-features.ts + packages/platform-objects/src/identity/sys-user.object.ts, packages/platform-objects/src/feature-gate-guard.test.ts, packages/spec/src/ui/action-params.zod.ts (validateActionParams), packages/runtime/src/action-execution.ts (resolveDeclaredActionParams, enforceActionParams, the api forward), packages/runtime/src/domains/actions.ts (the REST /actions door), packages/plugins/plugin-auth/src/auth-manager.ts (getPublicConfig), packages/plugins/plugin-auth/src/admin-user-endpoints.ts, ui/packages/app-shell/src/views/ActionParamDialog.tsx (filterVisibleParams, refusalParamMarks, handleSubmit), ui/packages/app-shell/src/console/AppContent.tsx (features), ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx (the api door). The rest of the diff (the changeset, the two new test files) is the lane seat's ACCEPT at TIER_DEFAULT.

Inputs: card #28280 (body, triage 6538087552, claim 6558617122, os-dev-report 6559354696), PR #28707 body, file list and net diff at the head above, the head's check-runs, and the live tree at origin/main da6f0ce35da6 read with git show. Check-runs at read time: 24 completed (success or skipped, none failed; Governed Surface Queue Guard, Check Changeset, PR-body guards, Spec property liveness, Dogfood Regression Gate, Auto Label, Check PR Size all success), 19 still in_progress (Build Core, Build UI Deps, Test Core 1-8, Live E2E UI, every Lint and Type Check lane). That is an honest reading, not a verdict on those lanes: no gate had answered red when this record was rendered.

① Derived judgments

  1. Accept set of the action door — NARROWED, and the PR does not say so. WRONG. sys_user.create_user is a declared same-origin type: 'api' action, so a Console click goes through POST /api/v1/actions/sys_user/create_user (useConsoleActionRuntime.tsx 425-440, as fix(console): Create User marks a missing email on its input, in the user's language #28649 measured), and both doors run the ADR-0104 D2 param contract BEFORE the in-process forward (domains/actions.ts 738; action-execution.ts 3753 then 3879). resolveDeclaredActionParams (action-execution.ts 2797-2815) emits one descriptor per declared param, in order, named p.name ?? p.field: with this diff that is email (required false) then email (required true). validateActionParams (action-params.zod.ts 185-193) folds them into declared = new Map(); for (const p of resolved) declared.set(p.name, p) — the LAST declaration wins, so the door's contract for email is required: true, unconditionally. enforceActionParams reads no visible and no features. Result on the tree: every bag without a present email is refused 400 with Invalid action params: Action param "email" is required, whatever phone sign-in says — including the phone-only create the endpoint accepts (admin-user-endpoints.ts 700-727) and the PR's own second scenario (phone sign-in on, Email optional, Phone Number filled). Before this diff the door passed that bag through. This is a server-side accept-set narrowing hidden behind a UI change; neither the PR body nor the changeset names it, and Clause-②: no is false for the tree as it stands (see ②). The PR's tests cannot see it: the app-shell test mocks the fetch at authFetchSpy (the door is never run), and the platform-objects pin evaluates gates only. Must-fix, in ④.
  2. Registry booking sys_user.actions.create_user.params.email under phoneNumber.gatedInputs — RIGHT as a booking. phoneNumber is opt-in, so featureGatePredicate('phoneNumber') is features.phoneNumber == true. The forward guard resolves the path to the first param whose name ?? field equals the path's last segment (resolveGatedInput, feature-gate-guard.test.ts 122) — the FIRST email param, which carries exactly that gate through requiresFeature lowering (lowerRequiresFeature, no existing visible, so source is string-equal to the gate). The reverse guard's walker matches features\.([A-Za-z0-9_]+) over every param's visible.source, so it books three references (features.phoneNumber == true; has(features.phoneNumber); features.phoneNumber != true) all under the one path, all found in the set. Neither guard's logic is edited; the diff to feature-gate-guard.test.ts is header prose only, so nothing is weakened. What the registry cannot say — and now silently does not — is that the SECOND param on the same path carries the negation; the row itself carries no note (should-fix, ④).
  3. Console rendering per features binding — RIGHT, with the default case marked. getPublicConfig() always serves phoneNumber: (pluginConfig as any).phoneNumber ?? false (auth-manager.ts 4620), a boolean on every deployment. AppContent.tsx 298-311 holds features as {} until getAuthConfig() resolves, then setFeatures(cfg.features ?? {}) whole; the Setup → Users page renders inside that provider (AppContent.tsx 1428; HomeLayout's empty features is the Home surface only). filterVisibleParams (ActionParamDialog.tsx 244-280) shows a param whose gate answers true or faults. So: default deployment after the fetch → { phoneNumber: false } → has() true, != true true → the REQUIRED param alone is shown, marked, and a blank one is refused client-side (handleSubmit 596-600). Phone sign-in on → the optional param alone, beside Phone Number. Before the fetch answers, or when it fails → {} → the optional param alone, unmarked, as on main today. The card's bug (default case unmarked) is fixed at the dialog; the has() deviation from the order's bare spelling is the right call, since a bare features.phoneNumber != true on {} faults and the fail-open rule would draw BOTH Email inputs (A2/U2 in the report agree with the code read).
  4. Exactly one email submitted — RIGHT. The dialog collects and serializes visibleParams only (serializeParamValues(visibleParams, values), 610), and both params share the key email, so the bag carries one email whichever is shown.
  5. fix(console): Create User marks a missing email on its input, in the user's language #28649's server-refusal marks — RIGHT, when the server is reached. refusalParamMarks (158-172) marks a required entry whose field is in collected, built from visibleParams names; the shown param is named email, so the endpoint's fields: [{ field: 'email', code: 'required' }] still lands on the input. But under finding 1 the door's 400 arrives first, as a bare sentence with no fields[], so with phone sign-in on the dialog shows the English Invalid action params… box instead of the fix(console): Create User marks a missing email on its input, in the user's language #28649 mark — a regression of fix(console): Create User marks a missing email on its input, in the user's language #28649's second case (phone on marks Email and Phone Number) on the live door.
  6. Endpoint refusal unchanged — RIGHT. No server file is in the diff; admin-user-endpoints.ts 717-725 still answers Email is required / Email or phone number is required with per-field entries.
  7. Declared public surface of @objectstack/spec — one tuple literal widened. PUBLIC_AUTH_FEATURES.phoneNumber.gatedInputs is as const, so the .d.ts literal type gains one member; no export added or removed (check:api-surface green locally, CI lane in progress). Precedent: fc4ac58 shipped the same kind of booking as patch, Clause-②: no.

② Semver level

  • .changeset/28280-create-user-email-required-mark.md: @objectstack/platform-objects patch, @objectstack/spec patch. Both ship dist; the spec kernel chunk carries the new path. For the registry line alone, patch matches precedent (fc4ac58) and is right.
  • Clause-②: no / Metadata-protocol: none on the PR body and the changeset: Metadata-protocol: none is right (no authorable key, value or meaning changes; requiresFeature and visible are existing keys). Clause-②: no is wrong for the tree as it stands: finding ① 1 narrows what POST /api/v1/actions/sys_user/create_user (and the MCP run_action door, same enforceActionParams) accepts — a phone-only bag that passed on main is refused at the door. A narrowing is Clause-②: no (narrowing) and BREAKING, with an ADR-0087 disposition, which no one wants for a p3 mark; the right outcome is a tree that does not narrow (④ must-fix), after which Clause-②: no and patch stand as declared.
  • check:dual-build-cjs-loads was NOT MEASURED locally (exit 3, needs every package dist). CI covers it: it is a step of ci.yml's Build Core job (line 3036), whose paths filter lists the script and every package source (line 408), and Build Core is in_progress on this head. The gate's verdict is CI's to give; the local non-measurement is honest and not a finding.

③ Boundary flags

  • Dev deviation 1 (has(features.phoneNumber) && added to the order's bare != true): answered, accepted. The code read in ① 3 agrees: on {} the bare gate faults, the dialog's fail-open rule shows the faulting param, and two Email inputs render. The has() term keeps the order's own hypothesis (one Email input) true in that state; the precedent spelling in sys-user.page.ts is the same shape. The first, optional param keeps the canonical gate the forward guard resolves, so option 1 is still option 1.
  • Dev deviation 2 (reverse-guard red cited from the card, not re-run): answered, accepted. The reverse guard's walker and the booking are read above; the three rows are green on the head by the report, and the guard logic is untouched.
  • Dev deviation 3 (model-free commit trailers): not a contract matter; noted only.
  • open_questions: [], out_of_scope_findings: []: nothing to escalate from the report. One boundary the report did not flag and this record raises: the action door's param contract (① 1). It was not in the order's file surface and the order forbade a runtime change, so it is escalated to the seat as the must-fix below rather than judged as a dev omission.
  • Triage's one-PR exception for public-auth-features.ts is honoured: the file changes by one value, no model change, and the guard header states the one-path, two-param booking as triage asked.

④ Design judgment

packages/spec/src/kernel/public-auth-features.ts — judged against the registry's own contract (its header: every gatedInputs path "must carry the matching predicate"; the forward and reverse guards in feature-gate-guard.test.ts) and its first consumers (lowerRequiresFeature, the guards, and the declaration in sys-user.object.ts the booking now stands for).

  • MUST-FIX — the two-param declaration the booking certifies narrows the action door's accept set (① 1). The registry line is only true of the declaration in sys-user.object.ts, and that declaration, read by resolveDeclaredActionParams + validateActionParams, makes email unconditionally required at POST /api/v1/actions/sys_user/create_user and at MCP run_action: validateActionParams keys descriptors by name and keeps the last, which is the required param. Concretely on the head: phone sign-in on, an administrator fills Phone Number and leaves the optional Email blank → the door answers 400 Invalid action params: Action param "email" is required before plugin-auth runs; the endpoint's own Email or phone number is required + fields[] never arrives, so fix(console): Create User marks a missing email on its input, in the user's language #28649's mark is lost too; a REST or MCP caller creating a phone-only user is refused where main accepts. The order's hypothesis ("the server interface is unchanged", PR body 风险与回滚) does not hold. The fix is the seat's call, and every route is outside this PR's declared file surface: (a) make the dispatch contract read the two declarations as one param whose required is the AND of both (a validateActionParams / resolveDeclaredActionParams change, packages/spec ui surface, contract review again), (b) drop the server-visible second declaration and carry the required mark another way, or (c) the card's option 2 (a required-when predicate), which triage set aside at p3. Whichever is chosen, the PR must either keep the door's accept set identical to main and prove it with a door-level test (a boot({ answer }) harness on api-action-forward.test.ts with the real SysUser declaration and a phone-only bag), or declare the narrowing honestly. As it stands: FAIL.
  • SHOULD-FIX — the booking row carries no note of what it stands for. The registry header and the gatedInputs docblock still promise that each path's target "carries the matching predicate", and the forward guard enforces that on the first email param only; the second param's features.phoneNumber != true is exactly the predicate a hide-when-on flag lowers to, booked under an opt-in flag. The guard header and the pin explain it, but the registry is the file a reader opens first. One comment on the row (// Two params on this path (#28280): the first carries this gate; the second is NEGATIVELY gated and pinned in platform-objects identity/sys-user-create-user-email-required.test.ts) keeps the file self-describing, inside triage's one-PR exception.
  • SHOULD-FIX — the pre-fetch / failed-fetch state is unmarked, and nothing says so to the administrator. With features {} the dialog draws the optional param and the endpoint still refuses a blank email. That is main's behaviour, not a regression, and the window is short (the fetch starts at AppContent mount); but the PR body's 风险与回滚 says the dialog marks Email "未开启手机号登录(默认)时", which is true only after /auth/config has answered. State it in the changeset body (it already does, one line) and in the 维护者速读; no code change asked.
  • The guard-test edit is prose only and weakens neither direction (① 2). The has() spelling is right (③). The client submits one email (① 4). The endpoint is untouched (① 6).

Implemented-by: claude/project-thread-5fg7d3
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: FAIL

…ly when every declaration is

sys_user.create_user declares email twice (optional while phone sign-in
is on, required while it is off). validateActionParams keyed params by
name with the last declaration winning, so the action doors refused a
phone-only create. A name is now required only when every declaration
of it is; the value shape is the first declaration's.

Co-Authored-By: Claude <noreply@anthropic.com>
…ng, semantic, spec minor)

Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9684fb3ba196ca4e8fd9a31ed3bffe40cac30ba8
Local-runs: none
Scope: packages/spec/src/kernel/public-auth-features.ts, packages/spec/src/ui/action-params.zod.ts + packages/runtime/src/action-execution.ts (resolveDeclaredActionParams, enforceActionParams, the MCP run_action path), packages/runtime/src/domains/actions.ts (the REST /actions door), packages/runtime/src/domains/api-action-create-user-email-param.test.ts, packages/spec/src/ui/action-params.test.ts, packages/platform-objects/src/identity/sys-user.object.ts, packages/platform-objects/src/feature-gate-guard.test.ts, packages/plugins/plugin-auth/src/admin-user-endpoints.ts, packages/lint/src/validate-action-dispatch-contract.ts, docs/adr/0104-field-runtime-value-shape-contract.md. The rest of the diff (the changeset, the platform-objects pin, the app-shell test) is the lane seat's ACCEPT at TIER_DEFAULT.

Delta record extending the base record on 2310b302 (PR comment 6559443163). Inputs: the base record; git diff 2310b302 9684fb3b (5 files, +206/-9: action-params.zod.ts, action-params.test.ts, public-auth-features.ts, the new runtime door test, the changeset); the live PR body at this head (both declaration lines rewritten); the dev's round-2 os-dev-report 6560214983 and the claim amendment 6560238686 on #28280; the head's check-runs at read time. Everything in the base record not restated here stands as judged there.

Check-runs on 9684fb3b at read time: every lane completed; Check Changeset ran twice — the 14:59Z run failed against the old PR body (Clause-②: no / Metadata-protocol: none against a changeset that now says yes (widening) / semantic), the 15:50Z run on the patched body is success. Every other completed run is success or skipped (Build Core, Test Core 1-8, Test UI 1-16, Live E2E UI, every Lint and Type Check lane, Governed Surface Queue Guard, Spec property liveness, PR-body guards, Dogfood Regression Gate). The stale failure row is the only non-green conclusion on the head, and the newer run of the same gate supersedes it. check:dual-build-cjs-loads, NOT MEASURED locally (exit 3 again), is a step of Build Core, which is success on this head: the gate is measured by CI.

① Derived judgments

  1. The must-fix of the base record — CLOSED, in the tree. validateActionParams (action-params.zod.ts 201-208) now folds same-name descriptors as declared.set(name, first ? { ...first, required: Boolean(first.required && p.required) } : p): a name is required only when EVERY declaration of it is required, and the value shape (type, multiple, options) is the FIRST declaration's. resolveDeclaredActionParams (unchanged, action-execution.ts 2788-2810) still emits both email descriptors in order (required false, then true), so the folded email is required: false, type email. enforceActionParams is the only caller (action-execution.ts 2877), reached from the REST door (domains/actions.ts 738) and the in-process/MCP path (action-execution.ts 3753); no other code on the head calls validateActionParams or resolveDeclaredActionParams (grep over packages/ and ui/: the remaining hits are prose in packages/lint/src/validate-action-dispatch-contract.ts, screen-input-contract.ts, action.zod.ts, an ADR and two READMEs, none of which states a last-wins or unique-name rule). So on the head: phone sign-in on, phone-only bag → the door passes it and forwards it to plugin-auth; phone sign-in off, blank email → the door passes it and the ENDPOINT's 400 VALIDATION_FAILED "Email is required" with fields[] is relayed whole (admin-user-endpoints.ts 717-725, untouched), so fix(console): Create User marks a missing email on its input, in the user's language #28649's per-field mark is restored on the live door; a non-string email is still refused at the door by the first declaration's email value shape. The accept set of the door for sys_user.create_user is main's again, and the endpoint stays the judge, which is the right split: the door reads neither visible nor features and cannot know which declaration the caller saw.
  2. Nothing else relied on last-wins — VERIFIED, as far as a text census goes. The dev reports an AST census (objectstack 430 arrays incl. ui/ and examples/, hotcrm 7, JSON 2) finding only create_user's email. This record re-ran an independent text-level census (every params: [ / actionParams: [ array literal in .ts/.tsx/.js/.mjs/.json/.yaml under the tree, top-level entries keyed by name or field) over the whole objectstack tree at 9684fb3b and hotcrm origin/main: 296 keyed arrays in objectstack, 7 in hotcrm, exactly one repeat — packages/platform-objects/src/identity/sys-user.object.ts:315 email x2. The two censuses count arrays differently (the AST pass counts arrays with no keyed entries too) and agree on the one finding. Not covered by either: params arrays assembled at runtime or stored in sys_metadata rows by a deployment; for those the change is a widening (a bag refused before is accepted now; nothing accepted before is refused), which is the direction the declaration names.
  3. The new door test (packages/runtime/src/domains/api-action-create-user-email-param.test.ts) — adequate for the must-fix. It drives HttpDispatcher.handleActions('/sys_user/create_user', 'POST', …) with the REAL SysUser declaration over a recording in-process target (the api-action-forward.test.ts harness shape), so enforceActionParams → resolveDeclaredActionParams → validateActionParams run on the real two-param declaration, and the forward to /api/v1/auth/admin/create-user is observed. Cases: the declaration carries [false, true]; phone-only passes and is forwarded with generatePassword: true; blank email passes the door and the endpoint's 400 (fields[] included) is relayed; an email is forwarded; a non-string email is refused at the door with nothing forwarded. Red-first on the base head is reported (2 of 5 red with the exact Action param "email" is required sentence this record predicted) and the merge was ablated back to red. The MCP run_action path is not driven, and that is right: create_user declares no ai.exposed, so systemObjectActionExposureError refuses it before enforceActionParams (action-execution.ts 3634ff, measured by the dev), and both doors share the one enforceActionParams. The spec-level pins in action-params.test.ts (optional-then-required and reversed → no issue; both required → required; first declaration's option set judges the value) cover the fold's three branches.
  4. Public surface of @objectstack/spec — one exported function's semantics change, no signature change. validateActionParams keeps its signature and export; check:api-surface has nothing to record; the Type Check lanes are green. The registry row change is as in the base record (literal tuple widened; patch precedent fc4ac58).
  5. ADR-0104 D2 — not reversed. D2 declares params[] a complete value contract enforced before the handler; it says nothing about a name declared twice (no "unique", "duplicate" or last-wins sentence in docs/adr/0104-…), so the fold is a definition where there was none, not a reversal needing an ADR (Prime Directive [WIP] Add Chinese version of the documentation #13). The docblock in action-params.zod.ts records the rule and its reason beside the code.

② Semver level

  • Changeset at head: @objectstack/spec minor, @objectstack/platform-objects patch; Clause-②: yes (widening), Metadata-protocol: semantic; PR body carries the same two lines and Check Changeset on the patched body is green.
  • Right level. Metadata-protocol: semantic is the honest kind: the meaning of params[].required on a twice-declared name changes at the doors (what an author wrote is read differently), and a kind takes at least minor on the package the diff moves — the fold lives in packages/spec/src/ui, so spec minor; platform-objects publishes no protocol change of its own (two declarations under existing keys) and keeps patch. Clause-②: yes (widening): the doors' accept set grows (a twice-declared name with one optional declaration no longer requires the key) and nothing previously accepted is refused — no narrowing arm, no BREAKING banner, no ADR-0087 disposition owed. Round 1's no / none / patch was wrong for round 1's tree (the base record's ② finding) and is withdrawn by this re-declaration.
  • Prose names the change honestly. The changeset's "The action doors (the semantic change)" paragraph and the PR body's 风险与回滚 and "What changes" each say what the fold is, why (the REST door refused a phone-only create with 400 VALIDATION_ERROR "Action param \"email\" is required"), that the door reads no visible/features so the endpoint stays the judge, and that no other action in the repo, its examples or hotcrm declares a name twice. The 维护者速读 now also says Email shows unmarked before /auth/config answers, as on main.

③ Boundary flags

  • Round-2 deviation 1 (REST door only; MCP cannot reach create_user): answered, accepted — ① 3.
  • Round-2 deviation 2 (the PR body written by the seat, not the dev): the live body at this head carries the revised text and declaration lines; Check Changeset confirms the body and changeset agree. Closed.
  • Round-1 deviation (the has() spelling): stands as accepted in the base record ③.
  • The claim amendment 6560238686 extends the file surface to action-params.zod.ts, action-params.test.ts and the runtime door test, and the seat extended the one-PR exception to action-params.zod.ts; the diff touches nothing outside that surface plus the base-record files. open_questions: [], out_of_scope_findings: []: nothing to escalate.
  • Base-record should-fix 1 (the registry row carries no note): CLOSED — public-auth-features.ts now states, on the phoneNumber row's notes, that the create_user email path stands for two params, the second negatively gated and why; the module header and the gatedInputs docblock say a path resolves to the FIRST param on its name or field and the gate is checked as an && conjunct, which is what resolveGatedInput and carriesGateConjunct do.
  • Base-record should-fix 2 (the pre-fetch {} state unmarked, unsaid): CLOSED — stated in the 维护者速读 and in the changeset's declaration paragraph. No code change was asked and none was made.
  • Carried: none.

④ Design judgment

packages/spec/src/ui/action-params.zod.ts — judged against ADR-0104 D2 (the dispatch contract it implements), its only consumer enforceActionParams on both doors, and the scenario it is accepted against (the two-param create_user declaration under phone sign-in on and off).

  • The fold holds. Required-only-when-every-declaration-is is the one reading that keeps the door from refusing a bag the endpoint accepts without the door pretending to know the caller's features; the alternative (first-wins) would make the declaration order a hidden contract on the door, and the previous last-wins made the order a hidden contract the other way (the base-record must-fix). The value shape from the first declaration is consistent with the forward guard's own first-param resolution and with the registry docblock now saying so; for create_user both declarations are the same field, so the shape is identical either way. Unknown-key handling is untouched, so the strict accept set for undeclared keys (D2's point) does not move. Tried to break it: a twice-declared name whose declarations differ in type would be judged by the first only — no such declaration exists (① 2), and the docblock states the rule, so a future author is told; a declaration repeated with BOTH required stays required (pinned). No finding.
  • Residual, not a finding for this PR: the spec's ActionSchema still accepts a twice-declared param name without saying anything, so the fold is the only place the meaning is defined. A parse-time note or a lint rule naming repeated names (and their intended per-gate use) would make the pattern discoverable before someone reads this docblock; that is a follow-up card, not a condition of landing.

packages/spec/src/kernel/public-auth-features.ts — as judged in the base record ④, with both should-fixes now closed in the file itself (the row notes, the header and the gatedInputs docblock). The forward and reverse guards are unchanged and hold (base record ① 2). No finding.

Implemented-by: claude/project-thread-5fg7d3
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

席位核对 · ACCEPT · PR #28707 · 2026-11-01T15:58Z

Head-sha: 9684fb3ba196ca4e8fd9a31ed3bffe40cac30ba8

维护者速读(席位终稿)

  • 改了什么:Setup → 用户 → 创建用户:未开启手机号登录(默认)时,邮箱在提交前就带必填星标;开启时邮箱仍可选,旁边是手机号。为此动作门的参数校验改一处语义:同名参数声明多次时,只有每一处都必填才算必填(以前最后一处说了算)。
  • 为什么改:Setup Create User: the Email field carries no required mark although the endpoint requires it when phone sign-in is off #28280:服务器在未开启手机号登录时拒收空邮箱,对话框却画成可选,管理员提交后才知道。第一轮的写法会让 REST 动作门把只填手机号的创建拒成 400,复核拦下(FAIL 6559443163),本轮修正。
  • 风险与回滚:中低。全仓(objectstack、hotcrm、JSON)只有 sys_user.create_user 重复声明参数名,别处行为不变;空邮箱仍由接口自己答 400 "Email is required"。回滚即 revert。
  • 先看:packages/spec/src/ui/action-params.zod.ts 的同名合并;packages/platform-objects/src/identity/sys-user.object.ts 的两个邮箱参数。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-create-user-email-param.test.ts(5 例,经真实 REST 动作门)。
  • 需要你做的:无。席位按常设授权落地,合并即关 Setup Create User: the Email field carries no required mark although the endpoint requires it when phone sign-in is off #28280。
  • 席位意见:契约复核 delta PASS,可落。

核对六项

  1. 形态:draft → main;Fixes #28280,合并关卡正确;闭合关键词另无命中。文件 9 个,在认领 6558617122 与修订 6560238686 申报面内(修订补入 action-params.zod.ts、其测试与 runtime 门测试)。PR 正文已由席位按 dev 稿重写(声明两行与 Verification 落在本 head)。
  2. 前提实测:dev 在 main 上实测对话框把邮箱画成可选、服务器拒空邮箱;round 2 的门测试经真实 HttpDispatcher REST 动作门与真实 SysUser 声明,红先、合并语义消融均红后恢复。MCP 门未驱动:create_user 无 ai.exposed,暴露拒绝先于参数检查,两门共用 enforceActionParams(复核认可)。
  3. 门禁:本 head check-runs 57 success、10 skipped(路径未命中)、0 待跑;Check Changeset 14:59Z 的一次 failure 来自旧正文,已被 15:52Z 的 success 取代。dev 本地 86/87 派生门禁 exit 0,check:dual-build-cjs-loads 本地未测,由 CI Build Core(success)读取;spec、runtime、platform-objects 测试与 typecheck 绿。
  4. 接受的偏差:基础记录的两条 should-fix 已关闭,无遗留。复核记一条非阻断残留:ActionSchema 静默接受重复参数名,合并语义成了"重复"唯一的定义;席位另立卡跟进。
  5. 范围外去向:上述残留由席位立卡(p3,server)。PR-body guards 本 head success。
  6. 落地条款:Clause-②: yes (widening) + packages/spec 路径 ⇒ CONTRACT_REVIEW_TIER 完整复核:基础记录 FAIL(6559443163,2310b302,must-fix 为门的接受集收窄)→ delta PASS(6560309650,Head-sha 与现 head 一致)。Metadata-protocol: semantic ⇒ spec minor、platform-objects patch,与 changeset 一致(Check Changeset 本 head 最新 success)。check-governed-merges --pr 28707:NOT governed,528 行。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 15:59
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 15:59
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 8bceb6a Nov 1, 2026
68 of 69 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-5fg7d3 branch November 1, 2026 16:21
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:ui size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setup Create User: the Email field carries no required mark although the endpoint requires it when phone sign-in is off

1 participant