Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .changeset/24230-marketplace-installed-list-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
'@objectstack/spec': minor
---

feat(spec)!: `marketplace:installed-list` is retired — refused by name at the schema door; the separate Installed Apps view it drew was retired, and Setup → Packages filtered to Source = marketplace replaces it (#24230, ADR-0049)

Clause-②: no (narrowing)

<!-- adr-0087: registered ui-marketplace-installed-list-retired -->

**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings (the `user:profile`, `element:filter`, `element:form`, `ai:chat_window` and `record:json` retirements shipped the same way).

`marketplace:installed-list` was the plugin console widget that drew the separate Installed Apps view. It was published in 17.3.0 with an empty `ComponentPropsMap` row and was never a `PageComponentType` member: it reached the page schema through the type union's open string arm. The separate view was then retired, because Setup → Packages carries the same packages and their actions behind a Source = marketplace filter. The widget's one page, `marketplace_installed`, had already left `@objectstack/cloud-connection`, whose "Installed Apps" navigation entry opens the filtered Packages view instead, and the console deleted the widget's one renderer. After that, an authored node validated clean and then drew "Unknown component type" in front of a user. A declared element nothing draws is a capability claim nothing keeps, so it is retired under ADR-0049 enforce-or-remove and refused by name, the `ai:chat_window` way.

**What is refused:** an authored `marketplace:installed-list` component node, at `PageComponentSchema.type`. That covers `definePage()`, `PageSchema`, and every door that parses pages: `os validate`, `os build`, `os lint` and the metadata save door. The issue is located at the node's own path, with `code: 'custom'` and `params.retiredComponentType`, and its message is the retirement prescription. `ComponentPropsMap['marketplace:installed-list']` stays as a row, because for a type outside the enum the row is the only thing that keeps the name known to the component-props gate, `check-yaml-examples` and the type vocabulary. The row now refuses every props bag, `{}` included, with the same prescription. One prescription string, `RETIRED_PAGE_COMPONENT_TYPES` in `@objectstack/spec/ui`, answers at every door. The authoring rule `component-type-unknown` reports the node by name, and the typo suggester no longer offers it.

**What else moves:** its print classification row (`PRINT_REFUSED_PAGE_COMPONENT_TYPES`) leaves with it, because a retired type is refused at the parse before any print rule reads it. The D3 entry `ui-cloud-connection-widgets-unknown-keys-refused`, which closed this type's props bag to `{}` in the same major, now speaks for `cloud-connection:panel` alone, so the upgrade guide no longer tells an upgrader to keep a node that is refused. No export is removed: the row was built by the `emptyProps` factory, which exports no def, so no JSON Schema leaves the published set.

**What stays accepted:** every other `ComponentPropsMap` row, byte-identically, including the sibling console widgets `cloud-connection:panel` and `mcp:connect-agent`. The open string arm also stays open: custom and plugin-registered types keep parsing, the `marketplace:` namespace included. The only string refused is the retired name itself.

## FROM → TO

| you wrote | write instead |
|:--|:--|
| a `{ type: 'marketplace:installed-list' }` component node in a page region, slot or container | nothing: delete the node. The Installed Apps list is Setup → Packages filtered to Source = marketplace |
| a page whose only job was to show that list | a navigation item `{ type: 'component', componentRef: 'developer:packages', params: { source: 'marketplace' } }`, which is what `@objectstack/cloud-connection`'s own "Installed Apps" entry does |

The one-line fix: delete the `marketplace:installed-list` component node, and link to the filtered Packages view if users need to reach it. No ADR-0087 conversion is registered, because the only edit is deleting an authored page node, and a mechanical conversion does not delete page nodes: which region closes up is a layout decision. The D3 entry `ui-marketplace-installed-list-retired` carries that delegation, so `os migrate meta --from 17` lists it as a manual change for every stack that still names the type.

## Who is affected, measured

- **objectstack** at `cbfa5e4335`: zero authored `marketplace:installed-list` nodes in shipped metadata. `@objectstack/cloud-connection` serves no page for it, and its pin test asserts that. The other hits were the spec's own row, print row and docblocks, its tests, two lint pins, one synthetic test fixture in `packages/cli` (now a live block) and the release-owned changelogs. A tree-scoped absence pin now holds that zero.
- **objectui** at the console pin `.objectui-sha` `5e15928b46`: the type is registered by string only, with no typed read of `ComponentPropsMap['marketplace:installed-list']`. The row's key is kept, so the console build at the pin is unaffected. objectui `main` has deleted the renderer.
- **Deployed metadata** was not measured.

The retirement kit:

- the retired-type map entry and the print classification row removed (`packages/spec/src/ui/page.zod.ts`), the row turned into a whole-bag refusal (`packages/spec/src/ui/component.zod.ts`), and the vocabulary docblock updated (`component-type-vocabulary.ts`)
- the D3 semantic entry `ui-marketplace-installed-list-retired` and its step-18 rationale fragment; the D3 entries `ui-cloud-connection-widgets-unknown-keys-refused` (narrowed to `cloud-connection:panel`) and `ui-mcp-connect-agent-unknown-keys-refused` (one clause) amended
- pin tests: `component-marketplace-installed-list-retirement.test.ts` pins `code`, `path`, `params` and the first sentence at each door (the map, the row with empty and keyed bags, `PageComponentSchema`, `PageSchema` at the element path), the absence from the enum and the print rows, the D3 registration and the narrowed sibling entry, with `cloud-connection:panel` as the live control, plus a tree-scoped absence walk; `component-type-vocabulary.test.ts` pins the type known only through its row, out of the typo candidates, and `marketplace:` still unreserved; the lint accept pins in `validate-component-types.test.ts` and `validate-component-props.test.ts` are flipped into refusal pins
- generated docs follow the schema: the reference page for `ui/page`, `spec-changes.json` and the upgrade guide
4 changes: 2 additions & 2 deletions content/docs/references/ui/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ View filter rule

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **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' \| 'record:approval_decision' \| 'record:approvals' \| 'record:attachments' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| '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; `ai:chat_window` — no renderer by design, the floating chat overlay is the AI chat entry point; `element:filter` and `element:form` — retired whole, no renderer for either ever shipped; `record:json` — the raw-JSON panel it was declared for was dropped) is refused at the parse itself, with the retirement prescription. |
| **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' \| 'record:approval_decision' \| 'record:approvals' \| 'record:attachments' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| '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; `ai:chat_window` — no renderer by design, the floating chat overlay is the AI chat entry point; `element:filter` and `element:form` — retired whole, no renderer for either ever shipped; `record:json` — the raw-JSON panel it was declared for was dropped; `marketplace:installed-list` — the separate Installed Apps view it drew was retired, Setup → Packages filtered to Source = marketplace replaces it) is refused at the parse itself, with the retirement prescription. |
| **id** | `string` | optional | Unique instance ID |
| **label** | `string \| Record<string, string>` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
| **properties** | `Record<string, any>` | optional (default: `{}`) | Component props passed to the widget. See component.zod.ts for schemas. |
Expand Down Expand Up @@ -432,7 +432,7 @@ Print declaration: paper size, orientation, margins, the running header and foot

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **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; `ai:chat_window` — no renderer by design, the floating chat overlay is the AI chat entry point; `element:filter` and `element:form` — retired whole, no renderer for either ever shipped; `record:json` — the raw-JSON panel it was declared for was dropped) is refused at the parse itself, with the retirement prescription. |
| **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; `ai:chat_window` — no renderer by design, the floating chat overlay is the AI chat entry point; `element:filter` and `element:form` — retired whole, no renderer for either ever shipped; `record:json` — the raw-JSON panel it was declared for was dropped; `marketplace:installed-list` — the separate Installed Apps view it drew was retired, Setup → Packages filtered to Source = marketplace replaces it) is refused at the parse itself, with the retirement prescription. |
| **id** | `string` | optional | Unique instance ID |
| **label** | `string \| Record<string, string>` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
| **properties** | `Record<string, any>` | optional (default: `{}`) | Component props passed to the widget. See component.zod.ts for schemas. |
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/test/platform-page-i18n-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,9 @@ describe('`pages.*` default-locale parity (#8764, moved off the nav roster by #1
// verdict moved without them is a verdict nobody has seen fail. The
// sample is a synthetic page shaped like the retired `marketplace_installed`
// page (#23933); it is a fixture, and no shipped population carries it.
// Its header copy is all the gate reads, so its body is a live block: the
// retired page's own `marketplace:installed-list` widget is refused by
// name since #24230.
const SAMPLE_PAGE = {
name: 'marketplace_installed',
label: 'Installed Apps',
Expand All @@ -1102,7 +1105,7 @@ describe('`pages.*` default-locale parity (#8764, moved off the nav roster by #1
},
],
},
{ name: 'main', components: [{ type: 'marketplace:installed-list', properties: {} }] },
{ name: 'main', components: [{ type: 'element:divider', properties: {} }] },
],
};
const SAMPLE_SOURCES = new Map([[SAMPLE_PAGE.name, pageSourceCopy(SAMPLE_PAGE)]]);
Expand Down
42 changes: 21 additions & 21 deletions packages/lint/src/validate-component-props.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1183,11 +1183,17 @@ describe('validateComponentProps — record:alert / record:quick_actions / recor
* open-string-arm types — never in `PageComponentType` — so nothing else
* judged them either), and the walker's unregistered-type skip swallowed the
* whole props bag: any authored key produced ZERO findings from
* validate/build. Their rows are strict and EMPTY — measured from the
* renderers' read points at the `.objectui-sha` pin, where both registrations
* discard the schema node (`() => <Widget />`) — so EVERY authored key is a
* finding. Remove either map row and its loud test here goes back to that
* silence.
* validate/build. `cloud-connection:panel`'s row is strict and EMPTY —
* measured from the renderer's read points at the `.objectui-sha` pin, where
* the registration discards the schema node (`() => <Widget />`) — so EVERY
* authored key is a finding. Remove the map row and its loud test here goes
* back to that silence.
*
* #24230 — `marketplace:installed-list` is RETIRED by name (its one renderer
* was deleted with the separate Installed Apps view). Its row is kept as a
* whole-bag refusal, so the same dispatch now refuses EVERY bag, `{}` included,
* as `component-props-invalid`; the type rule reports the node too (the
* map-driven `flags %s` case in `validate-component-types.test.ts`).
*/
describe('validateComponentProps — cloud-connection:panel / marketplace:installed-list are dispatched (#11575)', () => {
it('reports any key authored on `cloud-connection:panel`, naming the zero-prop surface', () => {
Expand All @@ -1204,25 +1210,19 @@ describe('validateComponentProps — cloud-connection:panel / marketplace:instal
expect(findings[0].message).toContain('cloud-connection:panel');
});

it('reports any key authored on `marketplace:installed-list` the same way', () => {
const findings = validateComponentProps(
stackWith([{
type: 'marketplace:installed-list',
properties: { filter: 'installed' },
}]),
);
expect(findings).toHaveLength(1);
expect(findings[0].rule).toBe(COMPONENT_PROPS_UNKNOWN_KEY);
expect(findings[0].where).toBe('page "probe_page" · marketplace:installed-list');
expect(findings[0].message).toContain('`filter`');
it('refuses the retired `marketplace:installed-list` bag WHOLE, `{}` included, as an error', () => {
for (const properties of [{}, { filter: 'installed' }]) {
const findings = validateComponentProps(
stackWith([{ type: 'marketplace:installed-list', properties }]),
);
expect(findings.map((f) => [f.rule, f.severity])).toEqual([[COMPONENT_PROPS_INVALID, 'error']]);
expect(findings[0].where).toBe('page "probe_page" · marketplace:installed-list');
}
});

it('stays silent on the empty bag both plugin-shipped pages author', () => {
it('stays silent on the empty bag the cloud-connection page authors', () => {
const findings = validateComponentProps(
stackWith([
{ type: 'cloud-connection:panel', properties: {} },
{ type: 'marketplace:installed-list', properties: {} },
]),
stackWith([{ type: 'cloud-connection:panel', properties: {} }]),
);
expect(findings).toEqual([]);
});
Expand Down
5 changes: 3 additions & 2 deletions packages/lint/src/validate-component-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,11 @@ describe('leaves the declared vocabulary and the open arm alone', () => {
// ⛔ The RETIRED types are deliberately NOT here — their kept
// `ComponentPropsMap` row makes them `isKnownComponentType`, and this rule
// used to read that as "accepted". They now have their own describe below.
// Plugin namespaces — the open arm's declared story.
// Plugin namespaces — the open arm's declared story. (`marketplace:installed-list`
// left this list when it was retired by name: an open-arm type, so the
// EXACT-name describe below is what reports it.)
'mcp:connect-agent',
'cloud-connection:panel',
'marketplace:installed-list',
// Colon-free custom/SDUI shapes — ditto.
'flex',
'grid',
Expand Down
Loading
Loading