Skip to content

Commit 97a2263

Browse files
claude[bot]claude
andauthored
feat(spec): user:profile is explicitly not author-placeable — refused by name at the schema door (#14159) (#15112)
* wip(spec): retire user:profile by name at the schema door (#14159) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i * wip(spec): user:profile pins green, generated artifacts regenerated (#14159) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i * feat(spec): user:profile is explicitly not author-placeable — refused 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fa8715a commit 97a2263

9 files changed

Lines changed: 371 additions & 15 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `user:profile` is explicitly not author-placeable — refused by name at the schema door (#14159, ADR-0049)
6+
7+
<!-- adr-0087: not-required (no-migration-prescription) The retired row declared zero keys, so there is no key to strip and no old shape to rewrite into a new one; the only edit an author can make is deleting the `user:profile` node, which the conversion layer deliberately does not do (the `element-filter-removed` docblock rules deleting authored page nodes out of a mechanical conversion — it is a layout decision, not a rewrite); and a whole-repo sweep measured zero authored instances across examples, packages, docs and stored metadata (Studio's palette has excluded the type since objectui 5e8965c). Nothing mechanical to prescribe and nothing to prescribe it to — the #7596 disposition, one surface over. -->
8+
9+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
10+
launch-window convention ships it as `minor`, as the `element:filter` /
11+
`element:form` element-grain retirements and the `targetVariable` key
12+
retirement did; the 17.0.0 cut itself carried the `group` / `guest` recipient
13+
removal and the `$in` / `$nin` / `$between` reference-position removal under
14+
`major` because that release was the major).
15+
16+
`user:profile` was declared in `PageComponentType` and carried an empty
17+
`ComponentPropsMap` row, and no renderer for it ever existed anywhere — not in
18+
objectui, framework or cloud (objectui#7135 measured the zero with a positive
19+
control in the same query shape; only the opt-in `PlaceholderRenderer`
20+
scaffold, which `apps/console` alone registers, ever drew it). objectstack#12183
21+
closed the same gap for its four siblings with renderers because real pages
22+
used them; this member had zero measured pull, and the director ruling of
23+
2026-09-01 (maintainer verbatim 「同意」) chose option B of that ask: a user
24+
profile is shell chrome — the signed-in user's avatar menu — which no mainstream
25+
product makes a page-placeable component, so the honest declaration is that it
26+
cannot be placed, not a renderer nobody asked for. Until now an authored
27+
`user:profile` node validated clean and drew `SchemaRenderer`'s red
28+
unknown-type panel in front of an end user in every host except the console
29+
(the ADR-0078 shape).
30+
31+
**What is refused:** an authored `user:profile` component node — at
32+
`PageComponentSchema.type` (so `definePage()`, `PageSchema`, and every stack
33+
door that parses pages: `os validate`, `os build`, `os lint`, the metadata
34+
door), with a located issue (`code: 'custom'`, `params.retiredComponentType`,
35+
the node's own path) whose message is the retirement prescription. The name is
36+
also refused by `PageComponentType`'s own error map when the enum is parsed
37+
alone, and `ComponentPropsMap['user:profile']` refuses every props bag —
38+
`{}` included — with the same prescription for the readers that dispatch on
39+
the row (the #5068 props gate, `check-yaml-examples`, the type vocabulary's
40+
known set). One prescription string (`RETIRED_PAGE_COMPONENT_TYPES`,
41+
`@objectstack/spec/ui`), three doors.
42+
43+
**What stays accepted:** every other member of `PageComponentType` and
44+
`ComponentPropsMap`, byte-identically — `global:search` and
45+
`global:notifications` (the two shipped shell singletons), `app:launcher`,
46+
`nav:menu`, `nav:breadcrumb`, and the whole open string arm: custom and
47+
plugin-registered types keep parsing, stored documents keep loading. `user:`
48+
is no longer a namespace the enum populates, so the `component-type-unknown`
49+
authoring rule no longer claims it; the only string refused is the retired
50+
name itself.
51+
52+
**Fix:** delete the `user:profile` component node. There is nothing to write in
53+
its place — the app shell renders the profile menu on every page without an
54+
authored element. A future application that needs a page-placeable profile
55+
component files a feature card with its consumer; the refusal then flips
56+
additively (option A of the same ask). objectui's `PALETTE_EXCLUSIONS` entry is
57+
unchanged (correct either way — the ruling's point 3).
58+
59+
The retirement kit:
60+
61+
- the retired-type map + enum error map + node-level check
62+
(`packages/spec/src/ui/page.zod.ts`), the refusing row
63+
(`packages/spec/src/ui/component.zod.ts`)
64+
- pin tests (`component.test.ts``code` + `path` + first sentence at each of
65+
the three doors, positive controls, preservation of the other shell
66+
singletons, the open arm; `component-type-vocabulary.test.ts``user:`
67+
leaves the reserved-namespace derivation)
68+
- generated baselines/docs follow the schema (`authorable-surface/`,
69+
`json-schema.manifest/`, `api-surface/`, reference docs); the hand-written
70+
`content/docs/ui/pages.mdx` component list says the truth
71+
- no ADR-0087 conversion, by the disposition above: the row had no keys to
72+
strip, and a mechanical conversion does not delete authored page nodes.

content/docs/references/ui/page.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ View filter rule
235235

236236
| Property | Type | Required | Description |
237237
| :--- | :--- | :--- | :--- |
238-
| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:discussion' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. `object-grid`, `mcp:connect-agent`). The spec's own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by `os validate` / `os build` / `os lint` (rule `component-type-unknown`). |
238+
| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:discussion' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. `object-grid`, `mcp:connect-agent`). The spec's own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by `os validate` / `os build` / `os lint` (rule `component-type-unknown`); a type the vocabulary RETIRED by name (`user:profile` — shell chrome, not author-placeable) is refused at the parse itself, with the retirement prescription. |
239239
| **id** | `string` | optional | Unique instance ID |
240240
| **label** | `string \| Record<string, string>` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
241241
| **properties** | `Record<string, any>` | optional (default: `{}`) | Component props passed to the widget. See component.zod.ts for schemas. |
@@ -306,7 +306,6 @@ View filter rule
306306
* `nav:breadcrumb`
307307
* `global:search`
308308
* `global:notifications`
309-
* `user:profile`
310309
* `ai:chat_window`
311310
* `ai:suggestion`
312311
* `element:text`
@@ -334,7 +333,7 @@ View filter rule
334333

335334
| Property | Type | Required | Description |
336335
| :--- | :--- | :--- | :--- |
337-
| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string` || Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. `object-grid`, `mcp:connect-agent`). The spec's own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by `os validate` / `os build` / `os lint` (rule `component-type-unknown`). |
336+
| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string` || Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. `object-grid`, `mcp:connect-agent`). The spec's own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by `os validate` / `os build` / `os lint` (rule `component-type-unknown`); a type the vocabulary RETIRED by name (`user:profile` — shell chrome, not author-placeable) is refused at the parse itself, with the retirement prescription. |
338337
| **id** | `string` | optional | Unique instance ID |
339338
| **label** | `string \| Record<string, string>` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
340339
| **properties** | `Record<string, any>` | optional (default: `{}`) | Component props passed to the widget. See component.zod.ts for schemas. |

content/docs/ui/pages.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ The `type` field is a union of the standard `PageComponentType` enum and any cus
182182
- **Structure:** `page:header`, `page:footer`, `page:sidebar`, `page:tabs`, `page:accordion`, `page:card`, `page:section`
183183
- **Record context:** `record:details`, `record:highlights`, `record:related_list`, `record:activity`, `record:chatter`, `record:path`, `record:alert`, `record:quick_actions`, `record:reference_rail`, `record:history` — each renders from the record context a **record page** mounts, so they belong on a `type:'record'` page. A `kind:'react'` page mounts no such context and `os validate` rejects them there (see Validating metadata §10b)
184184
- **Navigation:** `app:launcher`, `nav:menu`, `nav:breadcrumb`
185-
- **Utility:** `global:search`, `global:notifications`, `user:profile`
185+
- **Utility:** `global:search`, `global:notifications``user:profile` is **not** author-placeable: it is shell chrome (the signed-in user's avatar menu, which the app shell renders itself on every page), no renderer exists for it by ruling (objectstack#14159 / objectui#7135), and an authored `user:profile` node is refused at the schema door — `definePage()`, `os validate`, `os build` — with that prescription at the node's path instead of drawing an unknown-type panel in front of a user
186186
- **AI:** `ai:chat_window`, `ai:suggestion`
187187
- **Elements:** `element:text`, `element:number`, `element:image`, `element:divider`, `element:button`, `element:record_picker`, `element:text_input` (`element:filter` and `element:form` were retired in v17.x — no renderer ever shipped for either. List surfaces own their filtering via a view's `userFilters` quick-filter bar or the list toolbar's filter builder; for forms use the object-bound `object-form` block, which is rendered and designer-publishable)
188188

packages/spec/api-surface/ui.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@
287287
"RECORD_CONTEXT_BLOCK_TAGS (const)",
288288
"RECORD_CONTEXT_TYPE_PREFIX (const)",
289289
"RESERVED_COMPONENT_TYPE_NAMESPACES (const)",
290+
"RETIRED_PAGE_COMPONENT_TYPES (const)",
290291
"ReactBlockDef (interface)",
291292
"ReactInteractionProp (interface)",
292293
"ReactPropKind (type)",

packages/spec/export-origins/ui.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@
287287
"RECORD_CONTEXT_BLOCK_TAGS": "src/ui/react-blocks.ts#RECORD_CONTEXT_BLOCK_TAGS (const)",
288288
"RECORD_CONTEXT_TYPE_PREFIX": "src/ui/react-blocks.ts#RECORD_CONTEXT_TYPE_PREFIX (const)",
289289
"RESERVED_COMPONENT_TYPE_NAMESPACES": "src/ui/component-type-vocabulary.ts#RESERVED_COMPONENT_TYPE_NAMESPACES (const)",
290+
"RETIRED_PAGE_COMPONENT_TYPES": "src/ui/page.zod.ts#RETIRED_PAGE_COMPONENT_TYPES (const)",
290291
"ReactBlockDef": "src/ui/react-blocks.ts#ReactBlockDef (interface)",
291292
"ReactInteractionProp": "src/ui/react-blocks.ts#ReactInteractionProp (interface)",
292293
"ReactPropKind": "src/ui/react-blocks.ts#ReactPropKind (type)",

packages/spec/src/ui/component-type-vocabulary.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ describe('RESERVED_COMPONENT_TYPE_NAMESPACES is derived from the enum', () => {
2525
// A new namespace appearing here is a NEW VOCABULARY CLAIM — the
2626
// `component-type-unknown` rule starts refusing undeclared strings under
2727
// it the day the enum member lands. Update this list consciously.
28+
// `user` LEFT this list at #14159: `user:profile` was the namespace's only
29+
// member and is retired by name (refused at the parse through
30+
// `RETIRED_PAGE_COMPONENT_TYPES`), so the enum no longer populates `user:`
31+
// and the `component-type-unknown` rule no longer claims it.
2832
expect([...RESERVED_COMPONENT_TYPE_NAMESPACES].sort()).toEqual([
29-
'ai', 'app', 'element', 'global', 'nav', 'page', 'record', 'user',
33+
'ai', 'app', 'element', 'global', 'nav', 'page', 'record',
3034
]);
3135
});
3236

@@ -66,6 +70,20 @@ describe('KNOWN_COMPONENT_TYPES covers every declared face', () => {
6670
expect(isKnownComponentType('global:search')).toBe(true);
6771
expect(isKnownComponentType('global:notifications')).toBe(true);
6872
});
73+
74+
/**
75+
* #14159 (ruling B): `user:profile` is retired BY NAME — out of the enum, so
76+
* `user:` is no longer a reserved namespace, but still KNOWN through its kept
77+
* `ComponentPropsMap` row (the `element:filter` / `element:form` shape), so
78+
* every reader that dispatches on the row keeps recognising the name. What
79+
* refuses it is the schema door (`PageComponentSchema.type`), not this
80+
* vocabulary — pinned in `component.test.ts`.
81+
*/
82+
it('user:profile is known through its kept row and refused by name at the parse (#14159)', () => {
83+
expect(PageComponentType.options).not.toContain('user:profile');
84+
expect(hasReservedComponentNamespace('user:profile')).toBe(false);
85+
expect(isKnownComponentType('user:profile')).toBe(true);
86+
});
6987
});
7088

7189
describe('STRING_ARM_REGISTERED_TYPES ledger discipline', () => {

0 commit comments

Comments
 (0)