feat(spec): user:profile is explicitly not author-placeable — refused by name at the schema door (#14159) - #15112
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
…14159) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
… by name at the schema door (#14159) Ruling B (director seat 2026-09-01, maintainer verbatim 「同意」): a user profile is shell chrome (the avatar menu), no renderer is built, and an authored user:profile node is refused at the parse with a located prescription instead of drawing the unknown-type panel in front of a user. - page.zod.ts: RETIRED_PAGE_COMPONENT_TYPES (retired type -> prescription), consulted by PageComponentType's own error map and by a check on PageComponentSchema.type so the open string arm cannot re-admit the name - component.zod.ts: the ComponentPropsMap row stays and refuses every bag (z.never, the retiredKey channel at element grain) - pins: code + path + first sentence at each door, controls, preservation - generated artifacts regenerated; content/docs/ui/pages.mdx says the truth - changeset: @objectstack/spec minor (launch-window convention), ADR-0087 not-required (no-migration-prescription), the #7596 disposition Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9c4c385505b5dd168a675838e772195826c090c8 && git checkout 9c4c385505b5dd168a675838e772195826c090c8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 35e94c96b8a4f6f3f8bbfbf898342b7d84076d02 bf4716f283b410d95464e0bc20f6742080b9df08 && git checkout -B drift-repro 35e94c96b8a4f6f3f8bbfbf898342b7d84076d02 && git merge --no-ff bf4716f283b410d95464e0bc20f6742080b9df08
node scripts/docs-audit/affected-docs.mjs --json 35e94c96b8a4f6f3f8bbfbf898342b7d84076d02
|
Contract review (Clause ②) — PASS · ACCEPTReviewer of record: the Head and window
The narrowing, verified in the diff
Report checklist
Seat ruling on the report's open question (open to the maintainer's veto)A — keep as landed. A stored page row carrying a Out-of-scope group (sweep criterion: doors the new node-level refusal could also close, and readers that still advertise retired names)
LandingAt all-green on
Generated by Claude Code |
|
Landing provenance — Landing window readings, all on head
Sequence: ready → squash auto-merge → Generated by Claude Code |
Fixes #14159
Clause ②: yes — path limb:
packages/spec/src/**(the SUSPECT surfacedispatch-gates --tiernames; no path-derived mandate); content limb yes: accept-set narrowing — an authoreduser:profileelement type is refused at the schema door, where it parsed clean before.Dispatched by the
domain:specPM seat (sessionsession_0174WZTU6XcFcS7g2kykC53i, seat post #6017),mode:subagent. Draft on purpose —needs:contract-reviewis hung on this PR and on the card; ⛔ never flipped to ready by this seat.The ruling (director seat 2026-09-01, batch #26, maintainer verbatim 「同意」 — comment 5494656898 on #14159, quoted untranslated where it is the operative criterion)
「1. B:spec 与
validate明示user:profile不可被作者放置 —— 编写期响亮拒绝(reference-diagnostics 家族),失败落在作者面前而不是用户面前的红面板; 2. 渲染器不建:零测量拉动 … 3.PALETTE_EXCLUSIONS条目不动; 4. 裁决理由入册:用户档案是 shell chrome(头像菜单),主流产品无一把它做成页面可放置组件 —— B 是对结构事实的诚实声明,不是能力欠账; 5. 路由:落点 spec + validate ⇒ 本仓domain:spec排队;条款②(明示拒绝 = accept-set 收窄)⇒ 契约复审档」What changed
One prescription string, three doors, no drift —
RETIRED_PAGE_COMPONENT_TYPES(packages/spec/src/ui/page.zod.ts, retired type → prescription) is the single source:PageComponentTypelosesuser:profile, and its ownerrormap consults the table keyed onissue.input— only a value that used to be legal gets the retirement prescription (code: 'invalid_value'); a stranger keeps zod's enum message. This is the enum-VALUE-narrowing spelling the file already uses forrecord:chatterposition(CHATTER_POSITION_RETIRED) and the master-detail form type (MASTER_DETAIL_FORM_TYPE_RETIRED).PageComponentSchema.typegains a check against the same table. This is the one new mechanism, and it is contained inpackages/spec/src/ui:typeisz.union([PageComponentType, z.string()]), and the open string arm admits every string the enum refuses, so the enum's error map alone cannot reach an author through the page door — the gap theelement:filter/element:formretirements recorded in their docblocks as "a node-level refusal is not expressible here". With the check, an authoreduser:profilenode is refused at its own path withcode: 'custom',params.retiredComponentType: 'user:profile', and the prescription as the message. Every string the table does not name keeps parsing; stored documents keep loading; thecomponent-type-unknownrule keeps its namespace claim.ComponentPropsMap['user:profile']stays a row (so the SDUI 组件 props 没有解析闸门:PageComponent.properties是开放 record,ComponentPropsMap的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 props gate,check-yaml-examplesand the type vocabulary keep recognising the name instead of skipping it as an unregistered custom string — theelement:filterargument) and becomesretiredComponentProps('user:profile'):z.nevercarrying the same prescription, refusing every bag including{}— theretiredKeychannel one grain wider (expected: 'never'/code: 'invalid_type'). The helper throws at module load if the table has no entry for the type, so the row and the door cannot drift apart.The empty-props docblock loses the member (
packages/spec/src/ui/component.zod.ts).content/docs/ui/pages.mdx(hand-written) says the truth on the Utility line; the generatedcontent/docs/references/ui/page.mdx,api-surface/ui.jsonandexport-origins/ui.jsonfollow the schema viacheck:generated --fix(second run exits 0).check:strictness-ledgerasked for no entry (the row isz.never, not an object site).Measured first, as asked: the repo retires a component type two ways — element grain (
element:filter/element:form: enum value dropped, row kept with every key aretiredKeytombstone, bare node still parses) and enum-value narrowing (aReadonlyMapof retired value → prescription on the enum's error map). Neither reaches an author throughPageComponentSchema.type, so the node-level check is the smallest addition that makes the ruling's "refused by name, at the author" true; it reuses the family's table shape rather than inventing a tombstone table.validatemeasurement — one defect, one door, no lint ruleObjectStackDefinitionSchema.pagesisz.array(PageSchema)(stack.zod.ts), so the schema-door refusal surfaces atos validatestep 2, before the authoring rules run. Measured through the source CLI (tsx packages/cli/bin/run-dev.js validate) on a fixture stack carrying one region node and one slot node (bothuser:profile), exit 1:--jsoncarries the same issues undererrors(the slot one nested inside the slot union'sinvalid_union.errors, paths relative to the union — a property ofPage.slots'z.union([PageComponentSchema, z.array(PageComponentSchema)]), not of this retirement; the text face renders the nested line with the absolute path). Control in the same run: the identical fixture withglobal:searchin both positions exits 0,Validation passed. Sovalidateneither swallows nor generalises the located message, andpackages/lint/src/validate-component-props.tsis untouched — the claim comment declared it conditionally and the condition did not hold.Verification record — at the final head
bf4716f2Every exit captured before any pipe (
cmd > log 2>&1; EXIT=$?); verdict lines quoted from the gates' own output.pnpm --filter @objectstack/spec build(underos-verify-lock.sh)VERDICT command-exit 0·DTS ⚡️ Build success·check-dts-emitted: 34/34vitest run --maxWorkers=2 src/ui/component.test.ts src/ui/component-type-vocabulary.test.ts src/ui/page.test.tsTest Files 3 passed (3) · Tests 335 passed (335)·VERDICT command-exit 0pnpm --filter @objectstack/spec typecheck(tsc + scripts + test layer)check:test-typecheck: OK — 54 file(s) / 261 error(s) / 145 pinned signature(s) held(no debt moved)pnpm --filter @objectstack/spec check:generatedafter--fix✓ All 15 generated artifacts are up to date.(api-surface, export-origins, docs were the stale three)@objectstack/lint:vitest run --maxWorkers=2+pnpm typecheck(under the lock)Test Files 94 passed (94) · Tests 2905 passed (2905)·VERDICT command-exit 0--no-inline-config) over the four edited TypeScript filespnpm lint(that one is CI's; type-aware linting is not enabled, so this diff moves no untouched file's verdict)node scripts/check-adr-0087-registration.mjs --base 29db3cd2✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 dispositionnode scripts/check-changeset-no-major.mjs --base 29db3cd2✓ This diff introduces no major bump.node scripts/check-system-context-census.mjs(after the last edit, no--fixneeded)OK — 106 elevation read sites in 20 packages across 45 files, all anchoredpnpm check:nul-bytesOK (scanned 8215 text file(s) … no raw ASCII control bytes)pnpm check:doc-authoring✓ 14685 customer-facing string(s) across 723 spec sources clean — no internal issue-id references(see deviation 1 below — it went red on the first spelling)node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths; 91 families derived from the merge base) run atbf4716f2NOT MEASURED locally, each by the gate's own words, all CI-owned:
check-test-completeness.mjs(exit 3: "grades a saved turbo run test log, and no log was named … record this gate as NOT MEASURED");check:react-declaration-parity(exit 1: "MANIFEST is not set … This gate did NOT run" — objectui'ssdui.manifest.json, an on-demand pin-bump gate by ruling);check:skill-examples(the skills+docs and spec-TSDoc surfaces ran; the client-SDK surface refused becausepackages/client-react/distis unbuilt here);check:dual-build-cjs-loadsandcheck:type-check-debt(exit 3PREREQUISITE NOT MET: they read every package's dist / built type entry points, and this worktree built only spec, formula, sdui-parser, lint, metadata-core).Shipped apps:
definePage()isPageSchema.parse(), so importing every*.page.tsunderexamples/**(21 modules: app-showcase, app-crm) through the freshly built@objectstack/specproves none authorsuser:profileunder the new schema —page modules: 21 · imported clean: 21 · failed/authoring user:profile: 0, with a positive control in the same run (definePageon auser:profilenode throws the prescription:true).os validatethrough the source CLI:app-crmandapp-todoexit 0✓ Validation passed;app-showcaseandapp-multi-packagecould not load their configs here (they import@objectstack/connector-*/@objectstack/typesdist this worktree did not build) — NOT MEASURED via the CLI, covered for pages by the module proof, andgit grep user:profilefinds zero authored nodes inexamples/**orpackages/**sources at the base.Reverse verification — one leg, from the committed state
Mutation: both schema files restored to the base commit
29db3cd2in the worktree only (git restore --source=BASE --worktree), landing proven by content (RETIRED_PAGE_COMPONENT_TYPESoccurrences: page.zod.ts 6 → 0, component.zod.ts 4 → 0; the enum line and theemptyPropsrow back). Same three test files under the lock: 6 failed, 329 passed — the four refusal pins in the #14159 describe (row,PageComponentSchema,PageSchema,PageComponentType) and the two vocabulary pins (userback in the reserved namespaces; the known-through-row pin) went red; the positive controls, the preservation pin and the open-arm pin stayed green. Restore:git checkout HEAD -- both pathsunder atrap, proven bygit diff HEAD --statempty,git status --porcelainempty, andgit hash-objectequal togit rev-parse HEAD:pathfor both files (page.zod.ts0f6e88ee…, component.zod.ts14d8bb44…at the then-head; the final commit re-touched page.zod.ts only for deviation 1). Direction observed: red, as expected.Changeset —
@objectstack/specminor, and whygroup/guestrecipient removal (ShareRecipientTypealignment) and [spec]FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596's$in/$nin/$betweenreference-position removal both sit under## 17.0.0 → ### Major Changes— the release that WAS the major. Every enforce-or-remove refusal landed since the cut ships asminorunder the lockstep launch-window convention (scripts/check-changeset-no-major.mjs, end condition at GA): theelement:filter(ADR-0049 candidate:element:filterdeclarestargetVariable(and possibly the whole element) with no renderer or reader in any repo #9220) andelement:form(ADR-0049 candidate:element:formdeclares a full form contract with no renderer registration found in objectstack/objectui #9249) element-grain retirements under## 17.1.0 → ### Minor Changes, thetargetVariablekey retirement, theAdvancedPluginLifecycleConfigretirement. This PR follows the post-cut convention:minorwith the**BREAKING**banner as the carrier; the no-major guard is green. The contract review judges.not-required (no-migration-prescription)— the [spec]FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596 disposition one surface over. The row declared zero keys, so there is nothing to strip; the only edit is deleting the node, whichelement-filter-removed's own docblock rules out of a mechanical conversion ("deleting authored page nodes is a layout decision"); zero authored instances measured across examples, packages, docs and stored metadata (Studio's palette has excluded the type since objectui 5e8965c). Consequence stated plainly for the review: a stored page row that somehow carries auser:profilenode is refused at rehydration with this prescription rather than converted. If the review wants a D2 strip conversion anyway, that is a follow-up entry inconversions/registry.ts+ the step chain — outside this card's declared surface.Deviations and notes for the contract review
check:doc-authoringrefuses citation-shaped tokens in text printed at the customer (maintainer ruling 2026-08-12, 「处理 issue 时犯的错应该总结成经验,保留 issue id没有意义」) and went red on exactly those three tokens, so the customer-facing string says "by ruling" and carries the ADR ids (ADR-0078, ADR-0049); the issue anchors live in the adjacent source comment and the enum docblock. The pin asserts the first sentence,ADR-0049, and (negative) no#NNNN.packages/spec/src/ui/component-type-vocabulary.tsis not edited (outside the declared surface). Its docblock says the row set is "exactly" the string-arm registrations pluselement:filter/element:form;user:profilenow joins that set, so the sentence is one member short. Report line, not an edit; [finding] bareelement:filter/element:formnodes still validate clean, and retired component types are offered as typo suggestions — the node-level refusal #14159 built could close both #15110 carries it.user:is no longer a reserved namespace —user:profilewas its only enum member, soRESERVED_COMPONENT_TYPE_NAMESPACES(derived) drops it and thecomponent-type-unknownrule no longer claimsuser:*strings. Pinned consciously incomponent-type-vocabulary.test.ts; the only string refused is the retired name itself, and the open-arm pin says so.format-type.test.tsPAGE_COMPONENTliteral (a formatter fixture that copies the enum as it was) still listsuser:profile; it is not the spec's list and is not compared to the live enum, so it is left alone.invalid_unionwith the prescription nested (rendered with the absolute path byformatZodIssue, formatZodError 把 union 分支的拒绝信息压成 "Invalid input" —— #4001 策展的散文在 CLI 路径上到不了作者 #4971 /os validate/os build用的是 CLI 自己的 formatZodErrors,它同样把 union 分支的处方裁掉 —— #4971 修的不是这条路径 #5341) — pre-existing behaviour of the slot union for every refusal, pinned as such.Out of scope
element:filter/element:formnodes still validate clean, and retired component types are offered as typo suggestions — the node-level refusal #14159 built could close both #15110 (finding, unassigned; [finding] bareelement:filter/element:formnodes still validate clean, and retired component types are offered as typo suggestions — the node-level refusal #14159 built could close both #15110 is not addressed here): bareelement:filter/element:formnodes still validate clean at every door, and retired types remain "Did you mean" candidates inKNOWN_COMPONENT_TYPE_CANDIDATES— the node-level refusal this PR builds could close both, but adding members is a Clause ② narrowing and a ruling, not a rider.⛔ Untouched, per the ruling and the dispatch: objectui (
PALETTE_EXCLUSIONSstays — point 3; objectui#7135 closes on B through its own lane), every other member ofPageComponentType/ComponentPropsMap,content/docs/releases,skills. No renderer, no placeholder, no scaffold.🤖 Generated with Claude Code
https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
Generated by Claude Code