From c6d3f38b2c4004268abdfd2bc44a718318c48146 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 07:06:32 +0000 Subject: [PATCH 1/2] feat(plugin-map)!: retire the bare `map` node type key; `object-map` is the one spelling Unregister `ComponentRegistry.register('map', ...)` (and with it `view:map`) from @object-ui/plugin-map, following the objectui#8008 family ruling that retired the bare `gantt` / `kanban` keys. No schema face declares `map` as a component node type, so unregistering is the whole retirement; the stored `NamedListView.type` value `map` is untouched (ObjectView / ListView already emit `object-map` for it). - console: drop the lazy `map` stub. - core: drop the `map` / `view:map` rows from RECORD_SOURCE_DATA_ARM_BY_TYPE. - cli: regenerate known-schema-types (bare `map` leaves the list). - prompts: teach `object-map` instead of `view:map` on the list-view Keys line. - README: one registered type; a bare array under `data` is not a record source. - tests: drop the retired rows from the registration / arm pins; add the retirement pin with a firing control on `object-map`. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- .changeset/10393-retire-bare-map-key.md | 53 +++++++ .github/prompts/component.prompt.md | 6 +- apps/console/src/register-plugins.ts | 9 +- packages/cli/src/utils/known-schema-types.ts | 1 - packages/core/src/utils/record-source.ts | 15 +- packages/plugin-map/README.md | 39 +++-- .../ObjectMap.schemaDataShorthand.test.tsx | 11 +- .../index.bareMapKeyRetired-10393.test.tsx | 134 ++++++++++++++++++ .../src/index.recordSourceInput-7470.test.tsx | 8 +- .../index.recordSourceInputs-10394.test.tsx | 18 +-- .../src/index.registration.test.tsx | 1 - packages/plugin-map/src/index.tsx | 58 +++++--- .../SchemaRenderer.dataPropArm-9571.test.tsx | 2 - ...enderer.legacyPropsDataStrip-9758.test.tsx | 2 - .../check-prompt-component-keys.test.ts | 4 +- 15 files changed, 284 insertions(+), 77 deletions(-) create mode 100644 .changeset/10393-retire-bare-map-key.md create mode 100644 packages/plugin-map/src/index.bareMapKeyRetired-10393.test.tsx diff --git a/.changeset/10393-retire-bare-map-key.md b/.changeset/10393-retire-bare-map-key.md new file mode 100644 index 0000000000..e95e46219f --- /dev/null +++ b/.changeset/10393-retire-bare-map-key.md @@ -0,0 +1,53 @@ +--- +'@object-ui/plugin-map': minor +'@object-ui/core': patch +'@object-ui/cli': patch +'@object-ui/console': patch +--- + +**BREAKING (node type key):** `@object-ui/plugin-map` no longer registers the bare +`map` node type key, and its namespaced twin `view:map` goes with it. A node +authored `"type": "map"` no longer resolves a renderer. Write +`"type": "object-map"` — the one spelling the plugin serves (objectui#10393, +executing the objectui#8008 family ruling of 2026-09-09 that retired the bare +`gantt` and `kanban` keys). + +**Why.** The registry mounted a `map` node while the published declaration refused +it: `ObjectMapSchema.type` is the literal `'object-map'` and `AnyComponentSchema` +has no `map` arm, so `safeValidateSchema` answered a `map` node with +`invalid_union` while the html tier accepted it. Two faces, opposite verdicts. +No schema face ever declared `map` as a component node type, so there is no arm +to turn into a named refusal: unregistering is the whole retirement, exactly as +it was for `gantt`. After it, the html tier (`validateTree` over the live +registry) reports a `map` node as `unknown-component`, matching the Zod face. + +**⛔ No stored document moves.** The string `map` names two different things at +two different layers, and only one of them is retiring: + +| layer | value | who writes it | retired? | +| --- | --- | --- | --- | +| stored `NamedListView.type` / `defaultViewType` | `"map"` | `CreateViewDialog`, persisted per tenant | **no — untouched** | +| node type key | `map`, `view:map` | hand-authored JSON | **yes** | + +`ObjectView` and `ListView` map a stored `map` view onto the node type they +render, and both already emit `object-map`, so every map view created through +the console keeps rendering. Nothing in a tenant database changes, and ⛔ nothing +should be migrated there. + +**What moved with it.** + +- `@object-ui/console` drops its lazy `map` / `view:map` registration stub; the + lazy `object-map` stub is unchanged. +- `@object-ui/core`: `recordSourceDataArmForType` no longer lists the `map` and + `view:map` rows, since no block is registered under either key. +- `@object-ui/cli`: the generated known-type list that `objectui check` reads no + longer contains bare `map`, so the check now flags it. ⚠️ `view:map` stays on + that list, because the opt-in protocol placeholder (`registerPlaceholders()` + in `@object-ui/components`) registers it — in the console a `view:map` node + renders that placeholder panel, not a map, and `objectui check` does not flag + it. Search documents for `view:map` directly. + +The `@object-ui/plugin-map` README now describes one registered type, and its +sentence claiming a bare array under `data` reaches the in-memory adapter is +corrected: a bare array under `data` is not a record source on the map +(objectui#8348), so inline rows belong under `staticData`. diff --git a/.github/prompts/component.prompt.md b/.github/prompts/component.prompt.md index 946054c7e5..7754747c8e 100644 --- a/.github/prompts/component.prompt.md +++ b/.github/prompts/component.prompt.md @@ -87,14 +87,14 @@ Responsible for rendering records. The specific `type` determines the Props cont > ⚠️ **A `Keys` entry is a REGISTRY key; a `Required Types` entry is a spec `type` value. They are > not the same vocabulary and they have diverged.** `{ "type": "kanban" }` inside a `ListView` > config is spec-valid, but the board component is registered as `object-kanban` — the namespaced -> `view:kanban` and `view:gantt` spellings retired with the bare `kanban` / `gantt` registrations and -> now answer only the opt-in protocol PLACEHOLDER panel. A document naming one passes +> `view:kanban`, `view:gantt` and `view:map` spellings retired with the bare `kanban` / `gantt` / `map` +> registrations and now answer only the opt-in protocol PLACEHOLDER panel. A document naming one passes > `objectui check` and then draws nothing. Write the key from the `Keys` bullet, and where a > presentation is a config value rather than a component, write it as a prop. Enforced by > `pnpm check:prompt-keys`. #### 1. List Views (Collection) -* **Keys:** `view:grid`, `object-kanban`, `view:map`, `view:calendar`, `object-gantt`, etc. +* **Keys:** `view:grid`, `object-kanban`, `object-map`, `view:calendar`, `object-gantt`, etc. * **Contract:** Must implement `ListViewComponentProps`. ```typescript type ListViewComponentProps = { diff --git a/apps/console/src/register-plugins.ts b/apps/console/src/register-plugins.ts index 936d138067..8a323f9f9d 100644 --- a/apps/console/src/register-plugins.ts +++ b/apps/console/src/register-plugins.ts @@ -44,10 +44,11 @@ ComponentRegistry.registerLazy('object-map', () => import('@object-ui/plugin-map namespace: 'plugin-map', category: 'view', }); -ComponentRegistry.registerLazy('map', () => import('@object-ui/plugin-map'), { - namespace: 'view', - category: 'view', -}); +// ⛔ The bare `map` node type key is RETIRED (objectui#10393, executing the +// objectui#8008 family ruling of 2026-09-09) — `object-map` above is the +// surviving spelling. The STORED `NamedListView.type` value `map` is a +// different layer and is untouched: `ObjectView`'s `switch (viewType)` already +// emits `object-map` for it. ComponentRegistry.registerLazy('object-tree', () => import('@object-ui/plugin-tree'), { namespace: 'plugin-tree', diff --git a/packages/cli/src/utils/known-schema-types.ts b/packages/cli/src/utils/known-schema-types.ts index 4612b4be18..bc010c7b33 100644 --- a/packages/cli/src/utils/known-schema-types.ts +++ b/packages/cli/src/utils/known-schema-types.ts @@ -231,7 +231,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'location', 'lookup', 'main', - 'map', 'mark', 'markdown', 'marketplace:installed-list', diff --git a/packages/core/src/utils/record-source.ts b/packages/core/src/utils/record-source.ts index 1ef24ff894..fd09b8b445 100644 --- a/packages/core/src/utils/record-source.ts +++ b/packages/core/src/utils/record-source.ts @@ -100,8 +100,9 @@ export function resolveRecordSourceObjectName( * a block the ruling does not decide, and it is reported rather than guessed. * * The arm is passed BY THE CALL SITE rather than looked up from `schema.type` - * on purpose. Every one of these renderers is registered twice — `object-grid` + * on purpose. Most of these renderers are registered twice — `object-grid` * and the `view:grid` alias `grid`, `object-calendar` and `calendar`, and so on + * (the bare `gantt` and `map` keys are retired, objectui#8008 / objectui#10393) * — so a node reaches the same component under either spelling, and a table * keyed by `type` would answer for one tag and silently miss the other. A * REQUIRED parameter makes the arm a compile-time obligation at each of the @@ -317,8 +318,8 @@ export function resolveRecordSourceConfig( * ## Why a type-keyed table exists beside the REQUIRED parameter * * {@link RecordSourceDataArm}'s docblock states, correctly, why - * {@link resolveRecordSourceConfig} takes the arm as a required PARAMETER: each - * of these renderers is registered under two spellings, a node reaches the same + * {@link resolveRecordSourceConfig} takes the arm as a required PARAMETER: most + * of these renderers are registered under two spellings, a node reaches the same * component under either, and a table keyed by `type` would answer for one tag * and silently miss the other. That argument is about a call site that already * has one block in front of it — there a parameter is strictly better, and it @@ -345,7 +346,7 @@ export function resolveRecordSourceConfig( * ⚠️ ONE `register()` CALL PRODUCES SEVERAL KEYS, and a row must be written for * each of them — `SchemaRenderer` looks this table up with the raw * `schema.type`, which is whatever spelling the author wrote. A registration - * with `namespace: 'view'` is reachable as `view:map` AND as bare `map`; one + * with `namespace: 'view'` is reachable as `view:calendar` AND as bare `calendar`; one * with `skipFallback` is reachable ONLY under its namespaced key. MEASURED via * `ComponentRegistry.getAllTypes()` with the five plugins loaded, grouped by * the renderer each key resolves to. @@ -366,7 +367,9 @@ export function resolveRecordSourceConfig( * it does not compute one. * * - `ObjectGrid.tsx`'s `getDataConfig` — `'view-data'`. - * - `ObjectMap.tsx`'s `getDataConfig` — `'view-data'`. + * - `ObjectMap.tsx`'s `getDataConfig` — `'view-data'`. One block spelling + * only: the bare `map` key (and with it `view:map`) is retired + * (objectui#10393). * - `ObjectGantt.tsx`'s `rawDataConfig` — `'view-data'`. One block spelling * only: the bare `gantt` key is retired (objectui#8008). * - `ObjectCalendar.tsx`'s `dataConfig` — `'array'`. @@ -383,8 +386,6 @@ const RECORD_SOURCE_DATA_ARM_BY_TYPE: Readonly **The bare `map` node type key is retired** (objectui#10393, following the +> objectui#8008 ruling that retired `gantt`). The registry used to accept a node +> authored `"type": "map"` (and its namespaced twin `view:map`) while the +> published declaration refused it — `ObjectMapSchema.type` is the literal +> `'object-map'` and no schema arm names `map` — so a validated document could not +> use the key the registry took. Write `"type": "object-map"`. +> +> ⚠️ The **stored view type** `"map"` — what a saved `listViews[].type` or +> `defaultViewType` holds — is a **different layer and is unchanged**. Do not +> rewrite it: `ObjectView` maps a stored `map` view onto the `object-map` node +> type, so no saved view moves. ## Installation @@ -88,12 +97,14 @@ is honoured as well. **The provider does not change which query keys apply** (objectui#9061, the port of objectui#8769). An authored `filter` and `sort` narrow and order the rows on -**every** provider, inline ones included — `staticData`, a bare array under -`data`, and `data: { provider: 'value', items }` all reach the same in-memory -adapter the other providers go through, so `filter` is evaluated with the same -matcher. Before objectui#9061 the inline provider skipped that query and plotted -every authored row with an authored `filter` silently dropped. The platform row -ceiling (2,000 drawn rows, with a footnote naming both numbers — objectui#7210, +**every** provider, inline ones included — `staticData` and +`data: { provider: 'value', items }` both reach the same in-memory adapter the +other providers go through, so `filter` is evaluated with the same matcher. A +bare array under `data` is not a record source on this map (objectui#8348): the +ladder falls through to `staticData`, then `objectName`, so inline rows belong +under `staticData`. Before objectui#9061 the inline provider skipped that query +and plotted every authored row with an authored `filter` silently dropped. The +platform row ceiling (2,000 drawn rows, with a footnote naming both numbers — objectui#7210, ruling a′) applies to inline rows too, and it is applied to the **filtered** set, never to the raw one: a large inline array that a `filter` cuts below the ceiling plots every matching row and shows no footnote. @@ -113,7 +124,7 @@ The declared configuration input. Every key is optional: | `latitudeField` | Record field holding the latitude. Needs `longitudeField` alongside it; both values must be numbers. | | `longitudeField` | Record field holding the longitude. | | `locationField` | Single field holding both coordinates — see the formats below. Used when the lat/lng pair yields nothing. | -| `titleField` | Field shown as the marker title. Omitted, the title is resolved by the object's own record-title precedence (`@object-ui/core`'s `getRecordDisplayName`, ADR-0079): the declared `nameField`, its deprecated `displayNameField` alias, the legacy `titleFormat` template, a type-aware pick from the object's fields, then name-ish keys read straight off the record — the rung that answers when no object definition reached the view, as `staticData` and an inline `data` array never fetch one. `Record #` is the floor; `Marker` is reached only by a record carrying no id at all. | +| `titleField` | Field shown as the marker title. Omitted, the title is resolved by the object's own record-title precedence (`@object-ui/core`'s `getRecordDisplayName`, ADR-0079): the declared `nameField`, its deprecated `displayNameField` alias, the legacy `titleFormat` template, a type-aware pick from the object's fields, then name-ish keys read straight off the record — the rung that answers when no object definition reached the view, as `staticData` and an inline `data: { provider: 'value', items }` configuration never fetch one. `Record #` is the floor; `Marker` is reached only by a record carrying no id at all. | | `descriptionField` | Field shown under the title in the marker popup. | | `zoom` | Zoom level. Declaring it opts this view out of the auto-fit (see below). | | `center` | `[latitude, longitude]` — a two-number **tuple**, latitude first. Declaring it opts this view out of the auto-fit. | @@ -212,7 +223,7 @@ declare const dataSource: ObjectMapProps['dataSource']; | Prop | Description | | --- | --- | | `schema` | The map schema — the keys above. | -| `dataSource` | Resolves the `object` provider. Not needed for `staticData` or an inline `data` array. | +| `dataSource` | Resolves the `object` provider. Not needed for `staticData` or an inline `data: { provider: 'value', items }` configuration. | | `className` | Classes for the wrapper around the map. | | `data` | Records to render directly, bypassing the component's own fetch — the shape `ListView` passes when it already holds the rows. Tracked live: passing a new array after mount (e.g. once a host's own in-flight query resolves) updates the map. | | `onMarkerClick` | Called with the clicked record. | diff --git a/packages/plugin-map/src/ObjectMap.schemaDataShorthand.test.tsx b/packages/plugin-map/src/ObjectMap.schemaDataShorthand.test.tsx index 62b5523650..5221121717 100644 --- a/packages/plugin-map/src/ObjectMap.schemaDataShorthand.test.tsx +++ b/packages/plugin-map/src/ObjectMap.schemaDataShorthand.test.tsx @@ -66,7 +66,7 @@ import { describe, it, expect, vi } from 'vitest'; import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; import { ComponentRegistry, recordSourceDataArmForType } from '@object-ui/core'; import { ObjectMap } from './ObjectMap'; -// Registers `object-map` and its `view:map` alias — row 3 renders through it. +// Registers `object-map` — row 3 renders through it. import './index'; import type { DataSource } from '@object-ui/types'; @@ -190,15 +190,14 @@ describe('ObjectMap — the bare-array `schema.data` shorthand is retired (objec // namespaced key and a bare one, and `SchemaRenderer` looks the arm up with // the raw `schema.type`. A key added without a row in // `recordSourceDataArmForType` turns this red instead of silently answering - // `'undeclared'` and keeping the prop seat. Unlike `plugin-grid`, this - // plugin claims the bare `map` key too — no `skipFallback` here. + // `'undeclared'` and keeping the prop seat. The bare `map` key and its + // `view:map` twin are retired (objectui#10393), so the group is exactly the + // one registration's two keys. const siblings = ComponentRegistry.getAllTypes().filter( (type) => ComponentRegistry.get(type) === ComponentRegistry.get('object-map'), ); - expect(siblings).toEqual( - expect.arrayContaining(['object-map', 'plugin-map:object-map', 'view:map', 'map']), - ); + expect([...siblings].sort()).toEqual(['object-map', 'plugin-map:object-map']); for (const type of siblings) { expect([type, recordSourceDataArmForType(type)]).toEqual([type, 'view-data']); } diff --git a/packages/plugin-map/src/index.bareMapKeyRetired-10393.test.tsx b/packages/plugin-map/src/index.bareMapKeyRetired-10393.test.tsx new file mode 100644 index 0000000000..114d00ffbe --- /dev/null +++ b/packages/plugin-map/src/index.bareMapKeyRetired-10393.test.tsx @@ -0,0 +1,134 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * The bare `map` NODE TYPE key is retired; `object-map` is the one spelling + * this plugin serves (objectui#10393, executing the objectui#8008 family ruling + * of 2026-09-09, route 3). + * + * ## ⚠️ Why this is a plain unregistration and not a named refusal + * + * `BaseSchema` is `.passthrough()`, so a dropped MEMBER key is KEPT, not + * refused, and a retirement that forgets it ships a document that validates + * green and renders nothing (objectui#7664). That hazard needs a schema face to + * arise on. This key never had one: `@object-ui/types` names the literal + * `'map'` only in the STORED view-type unions, never as a component node type, + * and `AnyComponentSchema` has no `map` arm. ⇒ Unregistering IS the + * retirement, exactly as it was for `gantt` (objectui#8008). + * + * ## What the retirement changes, measured on both authoring faces + * + * Before it, the two faces disagreed about one document: the html tier + * (`sdui-parser`'s `validateTree` over the live registry) accepted a `map` node + * because the registry mounted it, while the Zod face refused the same node + * with `invalid_union`. Row 2 pins that they now AGREE, and its control is the + * same content spelled `object-map`, which both faces accept. + * + * ## ⛔ The layer this does not touch + * + * `map` also names a STORED `NamedListView.type`. `plugin-view`'s `ObjectView` + * and `plugin-list`'s `ListView` map a stored `map` view onto the node type + * `object-map` already, so no saved view moves. That is pinned where it can be + * seen — `plugin-view`'s `ObjectView.mapFlatten.test.tsx` (a stored `map` view + * emits an `object-map` node) and this package's + * `ObjectMap.listViewMapConfigReach.test.tsx` (a `viewType: 'map'` list view + * draws its markers through `object-map`) — and deliberately not restated here. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// `src/index.tsx` pulls in the real map bindings; stub them so a plain module +// import needs no WebGL canvas (same stub as `index.registration.test.tsx`). +vi.mock('react-map-gl/maplibre', () => ({ + default: () => null, + Map: () => null, + NavigationControl: () => null, + Marker: () => null, + Popup: () => null, +})); + +import { ComponentRegistry } from '@object-ui/core'; +import { safeValidateSchema } from '@object-ui/types/zod'; +import { manifestFromConfigs, validateTree } from '@object-ui/sdui-parser'; +// Module scope, not a hook: this import IS the registration (AGENTS.md +// test-discipline section). +import './index'; +// @ts-expect-error — plain-JS shared helper, intentionally untyped (`allowJs: false`) +import { maskComments } from '../../../scripts/js-comment-mask.mjs'; + +/** Local annotation, since the import above is untyped — the call site stays checked. */ +const mask: (source: string) => string = maskComments; + +const INDEX_TSX = join(dirname(fileURLToPath(import.meta.url)), 'index.tsx'); + +/** One document, spelled under whichever type key the row names. */ +const node = (type: string) => ({ + type, + map: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name' }, + staticData: [{ id: '1', name: 'HQ', lat: 37.77, lng: -122.42 }], +}); + +const liveManifest = () => + manifestFromConfigs( + ComponentRegistry.getKnownTypes().map((type) => { + const meta = ComponentRegistry.getMeta(type); + return { type, namespace: meta?.namespace, isContainer: meta?.isContainer, inputs: meta?.inputs }; + }) as unknown as Parameters[0], + ); + +const htmlTierCodes = (type: string) => + validateTree(node(type) as never, liveManifest()).diagnostics.map((d) => d.code); + +describe('the bare `map` node type key is retired (objectui#10393)', () => { + it('1. `map` resolves to nothing, under the bare AND the namespaced spelling', () => { + // `register(key, C, { namespace })` stores BOTH, so checking one spelling + // would leave the other resolving. + expect(ComponentRegistry.has('map')).toBe(false); + expect(ComponentRegistry.has('view:map')).toBe(false); + expect(ComponentRegistry.getConfig('map', 'view')).toBeFalsy(); + }); + + it('1. FIRING CONTROL — `object-map` still resolves, so the `false` above is a reading', () => { + // Without this the assertion above would also pass against a registry that + // had failed to load this package at all. + expect(ComponentRegistry.has('object-map')).toBe(true); + expect(ComponentRegistry.getConfig('object-map', 'plugin-map')).toBeTruthy(); + }); + + it('2. both authoring faces now REFUSE a `map` node — the html tier by name, the Zod face as before', () => { + expect(htmlTierCodes('map')).toEqual(['unknown-component']); + expect(htmlTierCodes('view:map')).toEqual(['unknown-component']); + expect(safeValidateSchema(node('map')).success).toBe(false); + }); + + it('2. CONTROL — the same content spelled `object-map` passes both faces', () => { + expect(htmlTierCodes('object-map')).toEqual([]); + expect(safeValidateSchema(node('object-map')).success).toBe(true); + }); + + it('3. the source registers ONE key, measured off disk rather than through the registry', () => { + // The registry answer above is about this process; this one is about the + // file, so a registration added in a form the registry happens not to reach + // still shows up. Comments are STRIPPED first: the retirement left a + // tombstone docblock that quotes the removed + // `ComponentRegistry.register('map', …)` call verbatim, so a raw scan would + // read the retired key back out of the prose that records its removal. + const src = mask(readFileSync(INDEX_TSX, 'utf8')); + const keys = [...src.matchAll(/ComponentRegistry\.register\(\s*'([^']+)'/g)].map((m) => m[1]); + // Anti-vacuity for the extraction: a regex that matched nothing would make + // the equality below hold forever. + expect(keys.length).toBeGreaterThan(0); + expect(keys).toEqual(['object-map']); + // And the stripper is not eating the code: the surviving registration's own + // renderer name survives the strip. + expect(src).toContain('ObjectMapRenderer'); + }); +}); diff --git a/packages/plugin-map/src/index.recordSourceInput-7470.test.tsx b/packages/plugin-map/src/index.recordSourceInput-7470.test.tsx index 399b21c01c..e8cc37bfc3 100644 --- a/packages/plugin-map/src/index.recordSourceInput-7470.test.tsx +++ b/packages/plugin-map/src/index.recordSourceInput-7470.test.tsx @@ -12,7 +12,7 @@ * `ObjectMap`'s `getDataConfig` reads `data`, then `staticData`, then * `objectName`, and the `object-map` zod schema carries `requireRecordSource` * (objectui#6939): one of the three must be present, none is required alone. - * Both registration keys still declared `objectName` with `required: true`, so + * The registration still declared `objectName` with `required: true`, so * the registry said the opposite of the schema — and `sdui-parser`'s * `validateTree` turns a required input into a `missing-required-prop` ERROR, * which a `staticData`-only map drew while the schema accepted it. @@ -20,7 +20,8 @@ * Ruled (letter align): drop `required`, state the one-of rule in the * description, add no one-of vocabulary to the input declaration. * - * Rows, per registration key: + * Rows, on the one registration key (`object-map`; the bare `map` key is + * retired, objectui#10393): * 1. `objectName` is declared (non-vacuity: a wrong type/namespace read fails * here) and is not required. * 2. Its description names all three record sources. @@ -49,7 +50,6 @@ import './index'; const MAP_KEYS = [ { label: 'object-map', type: 'object-map', namespace: 'plugin-map' }, - { label: 'view:map', type: 'map', namespace: 'view' }, ] as const; const objectNameInput = (type: string, namespace: string) => @@ -57,7 +57,7 @@ const objectNameInput = (type: string, namespace: string) => (i: any) => i.name === 'objectName', ); -describe('objectui#7470 — map registrations do not declare objectName required', () => { +describe('objectui#7470 — the map registration does not declare objectName required', () => { it.each(MAP_KEYS)('$label — objectName is declared and not required', ({ type, namespace }) => { const input = objectNameInput(type, namespace); expect(input, `${type} declares objectName`).toBeDefined(); diff --git a/packages/plugin-map/src/index.recordSourceInputs-10394.test.tsx b/packages/plugin-map/src/index.recordSourceInputs-10394.test.tsx index fa0380dbbd..b32796dc73 100644 --- a/packages/plugin-map/src/index.recordSourceInputs-10394.test.tsx +++ b/packages/plugin-map/src/index.recordSourceInputs-10394.test.tsx @@ -7,7 +7,7 @@ */ /** - * objectui#10394 — both map registrations declare the two record sources + * objectui#10394 — the map registration declares the two record sources * `ObjectMapSchema` already declares: `data` and `staticData`. * * Without an input, `sdui-parser`'s `validateTree` reported a block authored on @@ -17,7 +17,8 @@ * * ## The rows * - * Declaration, per registration key: + * Declaration, on the one registration key (`object-map`; the bare `map` key + * is retired, objectui#10393): * 1. The html tier accepts a block authored on `staticData` alone, and on a * `data` configuration alone — no diagnostic at all. * 2. Control for row 1: a bogus key is still reported, on a node bound by @@ -28,10 +29,6 @@ * bare array under `data` draws `type-mismatch`. * 5. Each description names the position it is true about. * - * Once: both keys publish IDENTICAL lists. They are spelled inline twice (a - * shared spread would hide both from `check:component-surface-parity`), so - * this row is what keeps the two copies from drifting apart. - * * Behaviour, through the real `SchemaRenderer` — the claims the descriptions * make, measured rather than assumed: * 6. `data` wins over `staticData` and `objectName`; `staticData` wins over @@ -75,7 +72,6 @@ import './index'; const MAP_KEYS = [ { label: 'object-map', type: 'object-map', namespace: 'plugin-map' }, - { label: 'view:map', type: 'map', namespace: 'view' }, ] as const; const MAP = { latitudeField: 'latitude', longitudeField: 'longitude', titleField: 'name' }; @@ -118,7 +114,7 @@ const POSITION_PHRASES: Record = { staticData: ['read SECOND', 'a `data` configuration wins', '`objectName` is read AFTER it'], }; -describe('objectui#10394 — map registrations declare data and staticData', () => { +describe('objectui#10394 — the map registration declares data and staticData', () => { it.each(MAP_KEYS)('$label — the html tier accepts a staticData-only map', ({ type }) => { expect(diagnosticsOf({ type, map: MAP, staticData: STATIC_ROWS })).toEqual([]); }); @@ -164,12 +160,6 @@ describe('objectui#10394 — map registrations declare data and staticData', () } } }); - - it('both keys publish identical lists, so the two inline copies cannot drift', () => { - const [a, b] = MAP_KEYS.map(({ type, namespace }) => declaredInputs(type, namespace)); - expect(a.length, 'object-map declares no inputs at all').toBeGreaterThan(0); - expect(a).toEqual(b); - }); }); const makeDataSource = (): DataSource => diff --git a/packages/plugin-map/src/index.registration.test.tsx b/packages/plugin-map/src/index.registration.test.tsx index e6d6ae383c..3e146efedc 100644 --- a/packages/plugin-map/src/index.registration.test.tsx +++ b/packages/plugin-map/src/index.registration.test.tsx @@ -76,7 +76,6 @@ describe('plugin-map module load', () => { // reason — green because nothing happened, not because nothing printed. const { ComponentRegistry } = await import('@object-ui/core'); expect(ComponentRegistry.has('object-map', 'plugin-map')).toBe(true); - expect(ComponentRegistry.has('map', 'view')).toBe(true); const printed = NOISE_CHANNELS.flatMap((channel) => spies.get(channel)!.mock.calls.map((args) => `console.${channel}(${JSON.stringify(args)})`) diff --git a/packages/plugin-map/src/index.tsx b/packages/plugin-map/src/index.tsx index 61c57e6c7b..58dd616441 100644 --- a/packages/plugin-map/src/index.tsx +++ b/packages/plugin-map/src/index.tsx @@ -76,11 +76,10 @@ export const ObjectMapRenderer: React.FC = elementDataSourceBlock(({ schema // provider plots nothing here (`API provider not yet implemented for // ObjectMap`). // -// Both keys spell the list INLINE rather than spreading one shared constant: +// The list is spelled INLINE rather than spread from a shared constant: // `check:component-surface-parity` cannot name the entries of a spread, so a -// shared list would drop both registrations out of that reader's population. -// The two lists are pinned identical instead, beside the rest of this -// declaration, in `index.recordSourceInputs-10394.test.tsx`. +// spread list would drop this registration out of that reader's population. +// The declaration is pinned in `index.recordSourceInputs-10394.test.tsx`. ComponentRegistry.register('object-map', ObjectMapRenderer, { namespace: 'plugin-map', label: 'Object Map', @@ -93,16 +92,41 @@ ComponentRegistry.register('object-map', ObjectMapRenderer, { ], }); -// `map` publishes the same surface; whether the bare key survives at all is -// objectui#10393's question, not this list's. -ComponentRegistry.register('map', ObjectMapRenderer, { - namespace: 'view', - label: 'Map View', - category: 'view', - inputs: [ - { name: 'objectName', type: 'string', description: 'ObjectQL object name. The record source is one of `data`, `staticData` and `objectName`; the `object-map` schema refuses a block that declares none of them.' }, - { name: 'map', type: 'object', description: 'latitudeField, longitudeField, titleField' }, - { name: 'data', type: 'object', description: 'A `{ provider, … }` data-source configuration, read FIRST on the record-source ladder: a map carrying one never reaches `staticData` and never queries `objectName`. `{ provider: \'value\', items }` plots those rows and `{ provider: \'object\', object }` queries that object, both narrowed by `filter` and ordered by `sort`. The `api` provider is not implemented on the map and plots no markers. A bare array is not this key’s shape and is not a record source: the map falls through to `staticData`, then `objectName`, so inline rows belong under `staticData`.' }, - { name: 'staticData', type: 'array', description: 'Inline records, read SECOND on the record-source ladder: a `data` configuration wins and this key is then never reached, while `objectName` is read AFTER it, so a map carrying both plots these rows and never queries that object. `filter` and `sort` narrow and order these rows exactly as they do fetched ones.' }, - ], -}); +/** + * ⛔ The bare `map` node type key is RETIRED (objectui#10393, executing the + * objectui#8008 family ruling of 2026-09-09, route 3). `object-map` is the one + * spelling this plugin serves. + * + * ## What was here, and why it went + * + * `ComponentRegistry.register('map', ObjectMapRenderer, { namespace: 'view', + * ... })` — a second key on the SAME renderer, which stored both `view:map` + * and the bare `map` fallback. The declared face admitted only one of the two: + * `ObjectMapSchema.type` is the literal `'object-map'` and `AnyComponentSchema` + * has no `map` arm, so a node authored `type: 'map'` failed validation + * (`invalid_union`) while the registry mounted it. Two published faces, + * opposite verdicts — the shape objectui#8008 retired for `gantt`. + * + * ## Why unregistering is the whole retirement here — measured, not assumed + * + * ⚠️ `BaseSchema` closes with `[key: string]: any` and `BaseSchemaCore` ends + * `.passthrough()`, so a dropped MEMBER KEY is KEPT, not refused (the + * objectui#7664 failure). That hazard needs a schema face to arise on, and this + * TYPE NAME never had one: in `@object-ui/types` the literal `'map'` appears + * only in the stored view-type unions (`NamedListView.type`, + * `defaultViewType`), never as a component node type, against a firing control + * of two for `object-map` (`objectql.ts` + its Zod mirror). There is no arm to + * convert into a named refusal, so unregistering IS the retirement. + * + * ## ⛔ Two layers, and only one of them moved + * + * The string `map` also names a STORED `NamedListView.type` — the value + * `CreateViewDialog` writes and every tenant's database holds. That layer is + * untouched: `plugin-view`'s `ObjectView` and `plugin-list`'s `ListView` each + * map a stored `map` view onto the node type they emit (their `case 'map'` + * branches), and both already emit `object-map`. ⇒ Every map view any user + * ever created through the console already renders through the surviving + * spelling; this retirement moves zero stored documents. + * + * Pinned in `src/index.bareMapKeyRetired-10393.test.tsx`. + */ diff --git a/packages/react/src/__tests__/SchemaRenderer.dataPropArm-9571.test.tsx b/packages/react/src/__tests__/SchemaRenderer.dataPropArm-9571.test.tsx index 08e75acf09..d67bef6756 100644 --- a/packages/react/src/__tests__/SchemaRenderer.dataPropArm-9571.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.dataPropArm-9571.test.tsx @@ -87,8 +87,6 @@ const OBJECT_ARM_TYPES = [ 'view:grid', 'object-map', 'plugin-map:object-map', - 'view:map', - 'map', 'object-gantt', 'plugin-gantt:object-gantt', ] as const; diff --git a/packages/react/src/__tests__/SchemaRenderer.legacyPropsDataStrip-9758.test.tsx b/packages/react/src/__tests__/SchemaRenderer.legacyPropsDataStrip-9758.test.tsx index 1c494694c5..0d24fc3c6d 100644 --- a/packages/react/src/__tests__/SchemaRenderer.legacyPropsDataStrip-9758.test.tsx +++ b/packages/react/src/__tests__/SchemaRenderer.legacyPropsDataStrip-9758.test.tsx @@ -89,8 +89,6 @@ const OBJECT_ARM_TYPES = [ 'view:grid', 'object-map', 'plugin-map:object-map', - 'view:map', - 'map', 'object-gantt', 'plugin-gantt:object-gantt', ] as const; diff --git a/scripts/__tests__/check-prompt-component-keys.test.ts b/scripts/__tests__/check-prompt-component-keys.test.ts index b1d20324d0..8ac19b6dc7 100644 --- a/scripts/__tests__/check-prompt-component-keys.test.ts +++ b/scripts/__tests__/check-prompt-component-keys.test.ts @@ -572,7 +572,7 @@ describe('the prompt surface this gate now reads', () => { expect(sites.filter((s) => s.scope === 'keys-line').map((s) => s.key)).toEqual([ 'view:grid', 'object-kanban', - 'view:map', + 'object-map', 'view:calendar', 'object-gantt', 'view:simple', @@ -589,7 +589,7 @@ describe('the prompt surface this gate now reads', () => { // Each of these appears in the file, and each would be a finding if the gate // read it. That they are absent from the scan IS the prose-safety proof. const taught = new Set(scan().sites.map((s) => s.key)); - for (const tombstone of ['user:profile', 'ai:chat_window', 'view:kanban', 'view:gantt']) { + for (const tombstone of ['user:profile', 'ai:chat_window', 'view:kanban', 'view:gantt', 'view:map']) { expect(taught.has(tombstone), `${tombstone} must stay in prose, never in a judged list`).toBe(false); } const prompt = fs.readFileSync(path.join(repoRoot, PROMPT_DIR, 'component.prompt.md'), 'utf8'); From ec6e667dd5e70cf560d21c80b79018431fc6a36f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 08:05:46 +0000 Subject: [PATCH 2/2] docs(plugin-map,changeset): date the superseded map clause and correct the bare-array claim on two docs pages - .changeset/10392-registration-record-source-inputs.md: the sentence that the `map` registration declares `data` / `staticData` now reads as of its own change and carries a dated note naming the `map` retirement; frontmatter byte-identical. - content/docs/plugins/plugin-map.mdx and content/docs/fields/location.mdx: a bare array under `data` is not a record source on the map; inline rows go under `staticData` or a `data: { provider: 'value', items }` configuration, matching the README wording. - the retirement changeset names both. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- ...10392-registration-record-source-inputs.md | 21 +++++++++++++------ .changeset/10393-retire-bare-map-key.md | 11 +++++++++- content/docs/fields/location.mdx | 2 +- content/docs/plugins/plugin-map.mdx | 10 +++++---- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/.changeset/10392-registration-record-source-inputs.md b/.changeset/10392-registration-record-source-inputs.md index 0747221b78..46eaeda541 100644 --- a/.changeset/10392-registration-record-source-inputs.md +++ b/.changeset/10392-registration-record-source-inputs.md @@ -19,12 +19,21 @@ on the registrations disagreed with that rule in two ways: (objectui#10392). - `object-map`, `map` and `object-gantt` did not declare `data` or `staticData`, so the validator reported a block authored on either as an - unknown prop. Both are now declared, on the schema's own arms: `data` is a - `{ provider, … }` data-source configuration (an object, so a bare array there - now draws a type diagnostic, matching the schema), and `staticData` is an - array of records. Each description says what the renderer does with the key, - including that the map does not implement the `api` provider - (objectui#10394). + unknown prop. At this change both were declared on all three, on the schema's + own arms: `data` is a `{ provider, … }` data-source configuration (an object, + so a bare array there draws a type diagnostic, matching the schema), and + `staticData` is an array of records. Each description says what the renderer + does with the key, including that the map does not implement the `api` + provider (objectui#10394). + + ⚠️ **Dated note, 2026-09-25 — the bare `map` registration has since been + retired — objectui#10393.** Later in this same release `@object-ui/plugin-map` + stopped registering the bare `map` key (and its namespaced twin `view:map`), + so `map` declares nothing: a node authored `"type": "map"` resolves no + renderer, and the html tier reports it as `unknown-component`. The + `object-map` and `object-gantt` registrations keep both inputs exactly as + described above. The rest of this entry is kept as the reading of this + change; the objectui#10393 entry states what ships. The renderers' read order and the zod schemas are unchanged. The `@object-ui/plugin-gantt` README sentence that listed the registration's inputs diff --git a/.changeset/10393-retire-bare-map-key.md b/.changeset/10393-retire-bare-map-key.md index e95e46219f..a8488f7b07 100644 --- a/.changeset/10393-retire-bare-map-key.md +++ b/.changeset/10393-retire-bare-map-key.md @@ -50,4 +50,13 @@ should be migrated there. The `@object-ui/plugin-map` README now describes one registered type, and its sentence claiming a bare array under `data` reaches the in-memory adapter is corrected: a bare array under `data` is not a record source on the map -(objectui#8348), so inline rows belong under `staticData`. +(objectui#8348), so inline rows belong under `staticData`. The same false claim +is corrected on the two docs pages that carried it, `plugins/plugin-map.mdx` +and `fields/location.mdx`. + +**One pending entry in this same release is superseded in part.** The +objectui#10392 / #10394 entry (`10392-registration-record-source-inputs.md`) +says `object-map`, `map` and `object-gantt` gained `data` / `staticData` +inputs. It now reads as of its own change and carries a dated note naming this +card: `map` declares nothing after this retirement, while `object-map` and +`object-gantt` keep both inputs. diff --git a/content/docs/fields/location.mdx b/content/docs/fields/location.mdx index fc0d73ee2f..63db0383d1 100644 --- a/content/docs/fields/location.mdx +++ b/content/docs/fields/location.mdx @@ -131,7 +131,7 @@ the renderer: } ``` -**Note**: the map plots the records it fetches, so it takes an `objectName` (or an explicit `data` array) rather than a `bind` path, and it derives its markers from those records — a `markers` key on the node is not read. Every marker setting lives under the declared `map` input. +**Note**: the map plots the records it fetches, so it takes an `objectName` (or inline rows under `staticData`, or a `data: { provider: 'value', items }` configuration — a bare array under `data` is not a record source) rather than a `bind` path, and it derives its markers from those records — a `markers` key on the node is not read. Every marker setting lives under the declared `map` input. ## Validation diff --git a/content/docs/plugins/plugin-map.mdx b/content/docs/plugins/plugin-map.mdx index ac0434a28d..63eeb16538 100644 --- a/content/docs/plugins/plugin-map.mdx +++ b/content/docs/plugins/plugin-map.mdx @@ -120,10 +120,12 @@ const schema: ObjectMapSchema = { ``` `filter` and `sort` are not object-only keys (objectui#9061). They narrow and -order inline rows — `staticData`, a bare array under `data`, or -`data: { provider: 'value' }` — exactly as they narrow and order fetched ones, -and the platform row ceiling (2,000 plotted rows with a footnote naming both -numbers) applies to inline rows too. The ceiling is applied to the **filtered** +order inline rows — `staticData` or `data: { provider: 'value', items }` — +exactly as they narrow and order fetched ones. A bare array under `data` is not +a record source on the map (objectui#8348): the ladder falls through to +`staticData`, then `objectName`, so inline rows belong under `staticData`. The +platform row ceiling (2,000 plotted rows with a footnote naming both numbers) +applies to inline rows too. The ceiling is applied to the **filtered** set, so a large inline array that a `filter` cuts below the ceiling plots every matching row and shows no footnote. Inline rows reach the map as the in-memory adapter's own deep copy, so they must be JSON-serializable and a record handed