diff --git a/.changeset/17429-pin-bump-describe-corrections.md b/.changeset/17429-pin-bump-describe-corrections.md new file mode 100644 index 00000000000..3023d825cdf --- /dev/null +++ b/.changeset/17429-pin-bump-describe-corrections.md @@ -0,0 +1,14 @@ +--- +'@objectstack/spec': patch +--- + +Two published `describe` sentences that the `.objectui-sha` bump to objectui `87af769e9a3e` makes false are corrected against the new pin (#17429). + +`Clause-②: no` + +Both are claims about what the SHIPPED console renderer does, so the pin is what dates them — and both were re-read by executing the pinned tree, not by refreshing a sha. + +- `Dataset` measure `format`: the clause said "a datetime value ignores it". objectui#8352 lands inside the `53ded82bf7a4...87af769e9a3e` range and makes the datetime arm of `formatMeasureDate` honour the same two words the date arm does — `relative` through `formatRelativeDate`, `short` through `formatDateTime(v, { locale, style: 'compact' })`. A date PATTERN is still ignored on both arms, which is the half of the sentence that survives. +- `FormField.span`: the clause said only the widest container-query tier's class is emitted, so a `'full'` field took one cell of two at the 720px modal width (objectstack#17328). objectui#9244 / objectui#9253 (objectui `bd09957380`) are also inside the range: `spanLadderFor` now emits one clamped col-span class per multi-column tier, so `'full'` is the whole row at every multi-column tier. The `'auto'` half — textarea, markdown, html, richtext and repeater — re-measured unchanged at the new pin. + +No key, default, enum member or export moves: the same authored metadata is accepted and refused as before, and `content/docs/references/ui/{dataset,view}.mdx` are regenerated from these two sentences. diff --git a/.changeset/17645-sdui-parser-lockstep-port.md b/.changeset/17645-sdui-parser-lockstep-port.md new file mode 100644 index 00000000000..7c171fddfc4 --- /dev/null +++ b/.changeset/17645-sdui-parser-lockstep-port.md @@ -0,0 +1,14 @@ +--- +"@objectstack/sdui-parser": minor +--- + +The save gate now stamps `inert-quick-add` and `member-type-mismatch`, the two diagnostics that existed only in objectui's copy of this parser — so a page no longer saves clean here and renders with a different verdict there (#17645). + +The two copies of this parser owe each other one thing: byte agreement on the accepted grammar and on diagnostic codes. This copy runs the **save gate** and objectui's runs the **renderer**, so a code on one side only is a dialect — the author gets one reading when they save and another when the page draws, which is surface-dependent and therefore reaches them as intermittent. Measured at the ported revision: objectui stamped 26 codes, this copy stamped 24, and the missing two were exactly these. + +- **`inert-quick-add`** (warning) — `quickAdd` on `` reaches no control. The Quick Add button is gated on **both** `quickAdd` and an `onQuickAdd` handler, and `onQuickAdd` takes a function, which no page on this tier can write (this tier parses, it never executes) and which the board substitutes none of its own for. It **replaces** the `unknown-prop` this copy used to emit for the key, which was false against the contract: `ComponentPropsMap['object-kanban']` publishes `quickAdd`, so an author who checked the spec found the warning contradicted and kept a key that will never do anything. Asked ahead of the declaration lookup on purpose — the claim is about the render path, so declaring the key must not silently disarm it. A falsy value and an unevaluated braced expression are deliberately untouched. +- **`member-type-mismatch`** (warning; `error` when an `enum` arm is present) — the coarse type check one level down, over the member kind an input declares. This brings the `ManifestInput.of` key and its three readers with it: the validator, the serializer's canonicalization, and the codegen's element type. `of: 'string'` on an array input now types the members `string[]` in the generated `.d.ts` instead of `unknown[]`, and a member no declared arm accepts draws **one** diagnostic naming every offending position rather than one per member. + +**Nothing published changes shape for an input that declares no `of`.** The key is absent-means-undeclared: the validator checks no member, the codegen emits the unnarrowed element type, and `manifestFromConfigs` publishes no `of` at all, so an entry written before the key existed serializes byte-identically. Measured on the tracked `sdui.manifest.json`: 0 of 339 inputs declare `of`, and the artefact regenerates to the same sha256 across this change. + +⚠️ **Both new codes are diagnostics, not a new red gate.** Each is a warning, so `compile().ok` — the save gate's pass/fail — is unchanged, and a page that saves today still saves. Escalating an inert authored key to `error` is a separate question and belongs at the save gate, not here. diff --git a/.changeset/console-87af769e9a3e.md b/.changeset/console-87af769e9a3e.md new file mode 100644 index 00000000000..339bcbadf32 --- /dev/null +++ b/.changeset/console-87af769e9a3e.md @@ -0,0 +1,240 @@ +--- +"@objectstack/console": minor +--- + +Console (objectui) refreshed to `87af769e9a3e`. Frontend changes in this range: + +Derived from the changesets objectui declared over the range — 584 releasing of 891 changesets added across 1156 non-merge commits; omitted: 307 release-nothing changesets, 279 commits carrying no changeset (they ship no package code). + +- **minor** — **BREAKING** — a record id is a **string** wherever metadata names one, and on the last `DataSource` door (objectui#9511). An authored `recordId: 42` / `resourceId: 42` no longer… (objectui `87af769e9`) +- **minor** — Store a typed percent by shifting the decimal, not by dividing a float by 100 (objectui#9810, maintainer ruling batch #161 item 3, letter B). (objectui `1560d4682`) +- **minor** — `object-grid`'s `operations` block is now the CEILING over `rowActions`, instead of one half of a union with it. (objectui `c269ed9e0`) +- **minor** — **Breaking — the published `RootRedirect` is removed; `/` now has exactly one resolver.** (objectui `4274861f4`) +- **minor** — Report display sites follow the display locale instead of the literal tag `en-US` (objectui#10020). (objectui `f7af6372d`) +- **minor** — `ObjectGallerySchema.filter` is typed as the destination its own docblock names — `QueryParams['$filter']` — on both faces, the TS interface in `objectql.ts` and the zod mirror in… (objectui `7cbefa540`) +- **minor** — Render the environment admin's read-rate report from the usage endpoint's `readRate` reading (objectui#9954; maintainer ruling on cloud#2333, batch #164 item 3). (objectui `c698a814a`) +- **minor** — `DetailViewField` declares `dueLike?: boolean` — the published TypeScript twin now accepts the key its own validator already judged and its own renderer already honours (objectui#… (objectui `8b446f5d0`) +- **minor** — feat(console): a "Language" item on the profile page, writing the signed-in user's own `sys_user.locale` (objectui#7501). (objectui `c2f0f4832`) +- **minor** — `RecipientPickerField` gains a picker mode for the `field` sharing recipient (objectui#7613; maintainer ruling objectstack#14103, executor objectstack#15072). (objectui `23b99585f`) +- **minor** — The console's error-recovery exits follow the declared landing (objectui#7373). (objectui `3fa3b3eb6`) +- **minor** — Studio's "publish whole app" reports the runtime authoring gate's per-draft advisories (objectui#6965; server half objectstack#9343). (objectui `ce986aafc`) +- **minor** — Client-evaluated `ConditionBuilder` mounts declare the scope roots their own host binds (objectui#9856) — the declared cost of objectui#9645, now paid. (objectui `030a675b0`) +- **minor** — **BREAKING** — Build the chat runtime's discriminated tool parts at the PRODUCER, and delete the last `as any` on the `useChat` call (objectui#8426; director seat, decision batch #86, 2026-09-08… (objectui `4dbab84d4`) +- **minor** — Drop a saved view's row cap that the contract refuses, at the LOWERING layer every repaired read point sits under — and say so on the one path that has no renderer to say it (obje… (objectui `5eabe8646`) +- **minor** — **BREAKING** — Refuse `operators` on `object-grid` by name, and name the correct spelling (objectui#9739, maintainer ruling 2026-09-18, letter C). (objectui `6ee259a8f`) +- **minor** — Declare the row-click modifier payload on `ObjectDataTableSchema.onRowClick` (objectui#9799), so the object-arm face stops denying a second argument its node already receives. (objectui `3b6d53bc3`) +- **minor** — **BREAKING (authoring surface): `body` is no longer a child-list key. Author `children`.** (objectui `2acd8e109`) +- **minor** — Execute the declarative row-level `operation: 'update'` action (objectui#7551, the objectui half of objectstack#14092; consumes `@objectstack/spec` 17.3.0's `ActionSchema.operatio… (objectui `feac43909`) +- **minor** — `FlowRunner`: a run that ended with `outcome: 'refused'` renders as a Close-only notice (objectui#7707 — lane 3 of the maintainer ruling on objectstack#14945, decision batch #42;… (objectui `98a6bddf9`) +- **minor** — **`resolveRecordSourceConfig`'s `data` parameter now follows the arm it is already told, instead of contradicting it.** (objectui `ab856ed30`) +- **minor** — `object-form`'s two seed keys now merge PER MEMBER. `initialData` is registered as the "alternate spelling of `initialValues` … read FIRST", and every presentation arm implemented… (objectui `63bf47da6`) +- **minor** — The drill `filter[...]` URL dialect can spell IS NOT NULL (objectui#9508). (objectui `6c06f0b50`) +- **minor** — `SchemaRenderer` now applies the objectui#9571 authored-`data` strip to the legacy `props` alias bag as well, so all three authoring spellings of that key lose the React prop seat… (objectui `7649f4364`) +- **minor** — fix(app-shell): `ConditionBuilder`'s subject dropdown stops offering roots the host does not bind (objectui `aced50d2e`) +- **minor** — `record:*` blocks honour the `aria` bag the protocol declares on them, and `RecordComponentAriaProps.ariaLabel` states the contract's inline locale vocabulary instead of narrowing… (objectui `272a53066`) +- **minor** — `object-form.customFields` now MERGES over the metadata-generated field set, as its registered description always promised (objectui#9778, maintainer ruling 2026-09-18, director s… (objectui `5a311a38d`) +- **minor** — **BREAKING** — `useNavigationOverlay` stops reading the retired `navigation.view` key, and stops substituting an authored name for the navigation-MODE token (objectui `0c789a402`) +- **minor** — Stop `FilterConditionField` writing a `between` row into stored criteria until BOTH bounds are filled in (objectui#9914). (objectui `4d7d322aa`) +- **minor** — **BREAKING** — Converge the bare `dashboard` key on `plugin-dashboard`, and retire `view:dashboard` with a by-name tombstone (objectui#9533). (objectui `e356c39ee`) +- **minor** — The percent EDIT WIDGET reads `scale` for its fraction width, not `precision` (objectui#9568). (objectui `9aa2a573b`) +- **minor** — `objectui generate page` scaffolds its child list as `children` (objectui#9847). (objectui `d2f723fd1`) +- **minor** — The VS Code extension no longer ignores a child list spelled `children`, and everything the platform scaffolds now emits that spelling (objectui#7181). (objectui `4b5bb9525`) +- **minor** — Declare `scale` on `PercentFieldMetadata` (objectui#9784). (objectui `e7084269f`) +- **minor** — Give the `file` grid cell a per-file view/download affordance (objectui#9485). (objectui `f0f204677`) +- **minor** — Forward the row-click modifier payload through the three hops that were dropping it (objectui#9462), so Cmd/Ctrl/middle-click on a row reaches a host handler. (objectui `f0f3cd5e0`) +- **minor** — A scatter's two numeric axes now honour the spec `ChartAxis` every other chart family already honours, instead of dropping it. (objectui `1b969aec6`) +- **minor** — **BREAKING** — The schema-driven condition editor now lints in the scope its host declares (objectui#8167, director ruling of 2026-09-17, batch #150 item 5 letter B). (objectui `95121fb99`) +- **minor** — **BREAKING** — `useSpecGesture`'s `onGesture` fallback payload reports the DECLARED spec gesture at every arm, not the recognizer's own name (objectui#9691). (objectui `0ee6e316e`) +- **minor** — Take the percent cell's magnitude from the value, never from the column's name (objectui#9452). (objectui `e05553c46`) +- **minor** — **BREAKING** — `useSpecGesture` fires a swipe on MEMBERSHIP of the declared direction set (objectui#7974, maintainer ruling of decision batch #70). (objectui `1cfdff814`) +- **minor** — Derive the bulk executor's data-source face from `DataSource` and stop erasing the check at the hand-off (objectui#9722). (objectui `20b5e361e`) +- **minor** — Honour `columns[].collapsed` on the registered kanban board (objectui#9628). (objectui `dea17b469`) +- **minor** — **BREAKING if your code hands a numeric primary key to one of these four** — a record id at the `DataSource` boundary is a `string`, as `@objectstack/spec` declares every record d… (objectui `bbba09840`) +- **minor** — Declare `cardTitle` — the canonical card-title spelling — on `ObjectKanbanSchema` (objectui#9606, director seat decision batch #150 item 3 letter 1, maintainer approved 2026-09-17… (objectui `78a9c6744`) +- **minor** — **BREAKING** — Stop `collapsible` from honouring an authored `open`, and retire the declaration on both published faces (objectui#8236, ADR-0049 enforce-or-remove). (objectui `ee70287e4`) +- **minor** — **BREAKING** — Refuse `onNavigate` and `onAddComment` by name on the `detail-view` JSON authoring face (objectui#9447). (objectui `ac716fff4`) +- **minor** — Export `ObjectTreeSchema` from the `@object-ui/types` root barrel (objectui#9550) (objectui `bbe57fdd5`) +- **minor** — **`buttonVariant` becomes authorable on `toast` and `sonner`.** Both registrations now declare it in their registry `inputs`, as `type: 'enum'` over exactly the six values the TS… (objectui `72f55c9ec`) +- **minor** — Build history rows now state their item count in each language's own grammar (objectui#9266). (objectui `15b33aeb4`) +- **minor** — **BREAKING** — `ChatbotSchema` no longer accepts `body`, on either published face (objectui#8572). The chat API's body params are authored as `requestBody`, which is what the rend… (objectui `c42554e94`) +- **minor** — **BREAKING** — the calendar date aliases `dateField` and `endField` are retired at both faces (objectui#8355). They are now **declared refusals**: an authored value is rejected **… (objectui `474797d62`) +- **minor** — **BREAKING** — Declare the one handler key the `'tree-view'` renderer reads (objectui#7804, the `TreeViewSchema` slice). (objectui `604476d97`) +- **minor** — ⚠️ **Behaviour change in the metadata designer: a bare field reference typed into a hook's "Run only when (optional CEL)" is now an ERROR in the editor.** It was accepted. Read th… (objectui `16603b9c9`) +- **minor** — One home for the `datetime` display convention in the readonly field widgets, one face per register (objectui#8209, maintainer ruling batch #142 item 2). (objectui `ac0e39a84`) +- **minor** — `record:details` honours `hideEmpty` on a section again — an all-empty section hides itself, `hideEmpty: false` keeps its heading and skeleton (objectui `542718f45`) +- **minor** — **BREAKING** — `ObjectKanbanSchema` no longer accepts `allowCollapse`, on either published face (objectui#8801). (objectui `d234fa91e`) +- **minor** — **`ObjectView` honours a spec-shaped named list view** (objectui#8254, the renderer half objectui#7928's option A requires — decision batch #70, 2026-09-07 — before `ObjectViewSch… (objectui `5226263ef`) +- **minor** — **BREAKING** — `AIInsightsSchema` and the `ai-insights` node type are RETIRED from the published type face (objectui#8800, ADR-0049 enforce-or-remove). (objectui `1bd1be7e2`) +- **minor** — **BREAKING (shipped as `minor` — see below):** `list` and `timeline` now refuse both content channels by name. Neither renderer reads `body` or `children`, so both keys become `?:… (objectui `53374dc07`) +- **minor** — Two frozen cell-renderer censuses now read the registry instead of a literal (objectui#8734) (objectui `3ecc369bf`) +- **minor** — **BREAKING** (declared `minor` — this repo pins its major to `@objectstack`, so a breaking change ships as a minor with this banner; AGENTS.md §版本号策略): `WidgetInput.label`, `Widge… (objectui `335abea3e`) +- **minor** — `SchemaRenderer` no longer spreads an authored `data` key as a React prop for blocks whose published `data` row is the `ViewData` OBJECT arm (objectui#9571, ruling objectui#8348 Q… (objectui `f0f4d6c8e`) +- **minor** — **BREAKING** — fix(components): Tailwind no longer compiles this package's prose into the published stylesheet (objectui `f7fcc2cdb`) +- **minor** — **BREAKING** — All three percent surfaces read `scale` for their fraction width, not `precision` (objectui#9295). (objectui `4a94c38b0`) +- **minor** — **BREAKING** — Declare the five handler keys the `'list-view'` renderer reads (objectui#7804, the `ListViewSchema` slice). (objectui `f1cd29032`) +- **minor** — Say why a capability-gated action is missing, in the action designer (objectui#7234, maintainer ruling 2026-09-08, option B). (objectui `45889f8e9`) +- **minor** — `record:chatter` / `record:discussion` now read `feed.filterMode` and `feed.enableMentions` (objectui#8968). (objectui `88561fdc4`) +- **minor** — `EventHandlersSchema` is removed from `@object-ui/types` (objectui#6910). (objectui `40f34b4ba`) +- **minor** — `ObjectTreeProps.schema` is the published `object-tree` node instead of `any`, and `getTreeConfig`'s parameter with it (objectui#8655). (objectui `009f92d7a`) +- **minor** — The four plain `objectql.ts` node faces declare the nine handler keys their registered renderers read (objectui#7804, the `objectql.ts` slice): `ObjectFormSchema.onCancel` / `.onE… (objectui `8d50bc2bf`) +- **minor** — **BREAKING (shipped as `minor` — see below):** six component schemas now refuse both content channels by name. `text`, `image`, `icon`, `tabs`, `accordion` and `calendar` (and `ui… (objectui `b7479abc7`) +- **minor** — `UIActionSchema` declares the four keys the two action renderers were reading through `as any` — `disabled`, `recordIdField`, `resultDialog`, `undoable` (objectui#8648, the object… (objectui `f95b1409f`) +- **minor** — `DataTableSchema` declares the seven handler keys its registered renderer reads: `onAddRecord`, `onBatchSave`, `onCellChange`, `onColumnResize`, `onRowActionDef`, `onRowClick` and… (objectui `75fca9669`) +- **minor** — **`NamedListView` declares the 17 members the protocol declares on the same surface, and each one now has a read point** (objectui#8980, director-seat class-one adjudication of 20… (objectui `0e2ddd418`) +- **minor** — `InputShorthandSchema` and `UiCalendarSchema` are now named exports of `@object-ui/types` itself, not only of `@object-ui/types/form` and `@object-ui/types/zod` (objectui#9406). (objectui `bbc9dc34e`) +- **minor** — The ingestion choke point says out loud when it CANNOT fold a retired spelling (objectui#8938) (objectui `84defabb2`) +- **minor** — fix(plugin-calendar): type `ObjectCalendar` at the published `object-calendar` schema, and declare the `calendar` container (objectui `51e144eda`) +- **minor** — A record id is a `string` everywhere in the published types, as `@objectstack/spec` has always declared it. Three published declarations that admitted `number` no longer do. (objectui `72d65875c`) +- **minor** — `AppAction.items` 上的 `shortcut` 由「静默剥掉」改为「具名拒收」 (objectui `c9f9baedf`) +- **minor** — The drill "escape hatch" can spell an empty bucket: the `filter[...]` URL dialect grows an is-null operator on both sides plus a chip for it (objectui#9159). (objectui `136ff4bb3`) +- **minor** — **BREAKING** — Retire 23 measured-dead locale keys from all ten packs — two whole families and eleven individual leaves (objectui#8754; director seat summon #22, 2026-09-12, maintainer verbatim… (objectui `ef5200107`) +- **minor** — Give a read-only `file` field a per-file view/download affordance (objectui#9161). (objectui `6d5db7b17`) +- **minor** — `record:related_list` accepts `relationshipValueField`, and three record renderers stop erasing their own props annotation (objectui#8649). (objectui `541ce4e02`) +- **minor** — `list-view`: retire the legacy `title` alias from the export-filename read, and pin the `rowActionDefs` exemption at both of `ListView`'s read sites (objectui#8653, the objectui#8… (objectui `3a9ab021c`) +- **minor** — The wrong-layer root advisory asks the platform for its verdict instead of keeping a second copy of it (objectui#9318). (objectui `e3cb47624`) +- **minor** — The row/card click props on the view components now declare the modifier payload they have always been invoked with (objectui#9357). (objectui `502eb5880`) +- **minor** — **BREAKING** — Declare the two handler keys the `'detail'` renderer reads (objectui#7804, the `plugin-detail` slice; director seat, decision batch #69, 2026-09-07). (objectui `7ca6ddd4b`) +- **minor** — **BREAKING (scored `minor` per this repo's version-alignment convention)** — `KanbanRenderer` takes `onCardMove` as an explicit React prop, and the `object-kanban` document face t… (objectui `55f39ee90`) +- **minor** — `UseNavigationOverlayOptions.onRowClick` now declares the modifier payload it has always been called with (objectui#9357). (objectui `0ce32d514`) +- **minor** — Give the flow `end` node's inspector a typed control for `config.message`, the key a refused outcome requires (objectui#9336). (objectui `d27dcf2c9`) +- **minor** — A record page shows a discussion panel if and only if it composes one (objectui#7298). (objectui `7aaa89160`) +- **minor** — Take lucide's runtime `icons` record off the console's eager path (objectui#9251, maintainer ruling of 2026-09-13, decision batch #132 item 4). (objectui `67485872e`) +- **minor** — One record-overlay shell: all five list-type renderers honour all four overlay `navigation.mode` values (objectui#9299, director seat decision batch #128 item 1, 2026-09-13). (objectui `7098eed36`) +- **minor** — **BREAKING** — `RecordDetailsComponentProps` no longer declares `layout` (objectui#9040 item 1). (objectui `63fb72c42`) +- **minor** — An unbound map now REFUSES; coordinates are never guessed (objectui#8169, maintainer ruling 2026-09-07, decision batch #67, option B). (objectui `cb725e78f`) +- **minor** — **BREAKING** — The console's "app not available" screen now says what it measured, and the by-name app probe stopped folding four answers into one (objectui#9262). (objectui `2bf34f70c`) +- **minor** — Name two of objectui#8499's arms on the `@object-ui/types/zod` barrel, and record the other two as absent by decision (objectui#9067, director seat, decision batch #121 item 5, ma… (objectui `279e48e8c`) +- **minor** — An `onCardClick` supplied to an `object-kanban` board runs **once** per card click instead of twice, and the published declaration of the key grows the second parameter the surviv… (objectui `a272a4ffe`) +- **minor** — **BREAKING** — Retire the "Tremor/simple format" adapter in `ChartRenderer` — the `index`, `category` and `value` reads (objectui#8650, triage ruling `5619609278` on AGENTS.md #0.1: route to the… (objectui `bb383e83d`) +- …and 484 more releasing changesets in this range (list capped at 100; see the objectui range below). + +⚠️ 98 of these carry a breaking change: 98 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + +**In this console build, declared nowhere** — objectui merged 279 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. + +- _(no changeset)_ ci: one `Test` aggregator becomes the required test context, shards 4 -> 8, dist pins get their own job (#9584) (objectui `eb1c9f9d2`) +- _(no changeset)_ docs(skills,AGENTS): teach `action:button` + `actionType`, retire the `events` bag (#9592) (objectui `7550728a6`) +- _(no changeset)_ docs(census): state the no-changeset-for-tooling ruling in the census header (objectui#9795) (#10077) (objectui `3631937fc`) +- _(no changeset)_ refactor(scripts): the item-carrier disposition is RULED and says it is not a dialect (#10087) (objectui `205b97353`) +- _(no changeset)_ docs(guide): remove the lazy-loading promise nothing keeps from the schema-rendering guide (objectui#9989) (#10086) (objectui `66d870feb`) +- _(no changeset)_ docs(detail-view): teach `dueLike` where a detail-view field is authored (#10075) (objectui `af57dc729`) +- _(no changeset)_ gate(doc-types): walk packages/NAME/README.md, with its ruled DOC_TYPE_EXEMPTIONS entries (#9996) (objectui `6f76bb7ad`) +- _(no changeset)_ docs(skills): move the three published guides off the retired `dataSource` expression root (#9378) (objectui `8ec28d73d`) +- _(no changeset)_ docs(skills): page-builder.md names the channel that publishes expression roots (objectui#9672) (#9997) (objectui `078f2e4f7`) +- _(no changeset)_ docs(skills): gate the usePermissions example on can(), a boolean, and mark its fence (objectui#9671) (#9994) (objectui `3d72fb65f`) +- _(no changeset)_ docs(guide): user-state-persistence taught the rejected user_app_state shape (objectui#5950) (#10011) (objectui `ec1de927d`) +- _(no changeset)_ docs(changeset): four pending bodies cite the retire-vs-remove discriminator instead of restating it (#9970) (objectui `706e09f27`) +- _(no changeset)_ docs(scripts): the $-dialect census keeps its by-name self carve-out, with the reasons pinned (objectui#9891) (#9915) (objectui `2bc9829ea`) +- _(no changeset)_ fix(scripts): type-check-coverage stale-entry messages carry no card-ending keyword (#9898) (objectui `fb7dfedbb`) +- _(no changeset)_ chore(claude): allow-list the two landing REST calls in settings.json (objectui#9862) (#9900) (objectui `99bcde511`) +- _(no changeset)_ feat(scripts): read one changeset against itself, and date the contradiction (#9850) (objectui `5365b4c34`) +- _(no changeset)_ fix(scripts): the polarity census reads the optional marker as a decoration, not as part of the key (objectui#9794) (#9831) (objectui `3ae740c4a`) +- _(no changeset)_ docs(changeset): date the `MEMBER_PIN_EXEMPTION_CEILING` reading in the pending 8171 body (#9823) (objectui `26ac50369`) +- _(no changeset)_ docs(changeset): date the rotted "keeps its own copy" clause in the 5993 note (#9822) (objectui `aed4b4f71`) +- _(no changeset)_ docs(plugin-detail): state the Reference Rail opt-in and the option its count is read from (#9811) (objectui `78a0582ba`) +- _(no changeset)_ fix(scripts): the polarity census reads a key as a NAME, not as any lowercase token (#9793) (objectui `6a0d1a435`) +- _(no changeset)_ docs(changeset): correct the `SpinnerSchema` member attribution in the 5632 pending body (#9763) (objectui `d18322415`) +- _(no changeset)_ fix(scripts): forward-parity stale-entry messages carry no card-ending keyword (#9755) (objectui `2414e3751`) +- _(no changeset)_ test(ci-cd-doc): read the Playwright reporter through the shared comment mask (#9748) (objectui `5e8a31aa1`) +- _(no changeset)_ feat(gate): read BORN-FALSE claims — an address this change's own diff moves (#9744) (objectui `cbb2e45ac`) +- _(no changeset)_ fix(scripts): key the indirect registration bypass by collection, not by file (#9724) (objectui `64deb1603`) +- _(no changeset)_ fix(scripts): refuse a `-t` name filter that cannot match the title it spells (objectui#9660) (#9730) (objectui `10cc93b79`) +- _(no changeset)_ docs(changeset): date the two rotted present-tense declaration claims in the kanban pending entries (#9723) (objectui `2dbb49975`) +- _(no changeset)_ fix(scripts): derive the indirect registrations' namespace from the call, not from the hand-kept table (#9716) (objectui `4cf57b6da`) +- _(no changeset)_ docs(changeset): stop the pending 8499 entry publishing a stale registry size (#9715) (objectui `e3ff936ca`) +- _(no changeset)_ docs(changeset): retire two present-tense "in-flight PR" claims before they publish (objectui#9706) (#9714) (objectui `3172b85fa`) +- _(no changeset)_ fix(ci): the lockfile-dedupe gate reports on pull requests instead of blocking (#9707) (objectui `50e5cafe3`) +- _(no changeset)_ test(ci): widen the live-reading lock to what required jobs RUN, not just the workflows (#9696) (objectui `dd871fc08`) +- _(no changeset)_ docs(changeset): correct the AIInsights paragraph in the 8178 entry (objectui#9625) (#9694) (objectui `a961ad174`) +- _(no changeset)_ test(ci): take the live registry reading out of a REQUIRED context (objectui#9562) (#9690) (objectui `29a8a9526`) +- _(no changeset)_ docs(plugin-calendar): compile the Direct Component Usage block and gate it (#9678) (objectui `bb2d33570`) +- _(no changeset)_ docs(skills): auth-permissions stops teaching `dataSource` as the `data` expression root (objectui#9379) (#9669) (objectui `61b755346`) +- _(no changeset)_ feat(scripts): census every page key a renderer reads against PageSchema (objectui#9438) (#9670) (objectui `53f2b189e`) +- _(no changeset)_ fix(scripts): the vite resolve oracle stops writing its scratch root into the swept repo root (objectui#9468) (#9658) (objectui `e896c3899`) +- _(no changeset)_ docs(tooling): the handler-key gate names the owner its ledger consults, in all three places (objectui#9456) (#9657) (objectui `0fb382eca`) +- _(no changeset)_ fix(scripts): the spec-symbol ratchet detects its own dead anchor (objectui#9537) (#9646) (objectui `0b7be13ad`) +- _(no changeset)_ chore(labeler): delete the inert `designer` rule and the exemption it needed (objectui#7771) (#9644) (objectui `8ad231846`) +- _(no changeset)_ feat(scripts): gate a test source naming a changeset the tree carries (objectui#9583) (#9635) (objectui `dda8f3815`) +- _(no changeset)_ fix(e2e): root the live storage-state write and both storageState configs on their own file (#9636) (objectui `8d1242b58`) +- _(no changeset)_ fix(census): the continuation-scope docblocks stop crediting a guard that cannot fire there (#9632) (objectui `15f01223d`) +- _(no changeset)_ fix(scripts): lint-coverage's stale-entry message stops spelling a closing keyword before its anchor (objectui#9538) (#9595) (objectui `ff29450a9`) +- _(no changeset)_ fix(scripts): make the body-dialect census report the key population it counted over (#9599) (objectui `a5b660f21`) +- _(no changeset)_ docs(plugins): author the `listViews` filter operator in its canonical spelling (objectui#7993) (#9612) (objectui `fdbfe2302`) +- _(no changeset)_ docs(components): document `wrapperClass` and its new refusal on the five pages that omit it (#9614) (objectui `e0a87dbbd`) +- _(no changeset)_ fix(scripts): check:spec-symbols reads EVERY occurrence of a claim phrase, not the first (#9608) (objectui `253c31418`) +- _(no changeset)_ docs(agents): point AGENTS.md at the invocation guard pin test instead of counting its refusals (objectui#9505) (#9587) (objectui `163630bc9`) +- _(no changeset)_ feat(ci): deliver the changeset claim re-read onto the pull request (objectui#9140) (#9581) (objectui `f508000b5`) +- _(no changeset)_ test(scripts): stop pinning a pending changeset filename in the two gate suites (#9582) (objectui `29f4c0582`) +- _(no changeset)_ docs(agents): record the merge_group leg's SECOND refusal predicate (the contract-review carrier) (#9466) (objectui `4b9a0a8f0`) +- _(no changeset)_ fix(scripts): fail on a new bare-name registry collision (objectui#9264) (#9531) (objectui `2904c5c40`) +- _(no changeset)_ build(tsconfig): raise test-program `lib` to ES2022 across 31 packages (#9512) (objectui `4e96becf5`) +- _(no changeset)_ fix(scripts): label an `any` index signature `index signature`, not `return type` (#9510) (objectui `2e1d0f032`) +- _(no changeset)_ fix(devx): refuse an appended path filter that a baked positional already swallows (objectui#7814) (#9504) (objectui `8fa7d69af`) +- _(no changeset)_ docs(gate): name key-refusal as a class check-spec-range-floors deliberately does not judge (objectui#9036) (#9481) (objectui `02d424ab3`) +- _(no changeset)_ fix(tests): register @testing-library/jest-dom in three test programs' types (#9480) (objectui `360300fea`) +- _(no changeset)_ ci(coverage): give the instrumented lane its own per-test budget so the coverage gate can run (#9474) (objectui `511e4024a`) +- _(no changeset)_ test(docs): pin command parity for every ci-cd-pipeline.md section by default (#9467) (objectui `db6aa19a9`) +- _(no changeset)_ docs: finish objectui#9297 — schema-rendering.md stops teaching silence, and the expression sandbox states its real allowlist (#9461) (objectui `75fc9df6e`) +- _(no changeset)_ fix(devx): resolve spec export conditions in the map's key order, and say which arm won (#9455) (objectui `72932dfcd`) +- _(no changeset)_ docs: publish each page's own values through the scope channel on the three remaining teaching surfaces (#9376) (objectui `035d3fac3`) +- _(no changeset)_ docs(fields): blank line before `## Field Schema` on four field pages (#9435) (objectui `63d9ca6f4`) +- _(no changeset)_ refactor(tsconfig): rename tsconfig.base.json to what it is (objectui#9330) (#9426) (objectui `6c7319753`) +- _(no changeset)_ docs(api): stop teaching quickAdd and allowCollapse on the object-kanban table (#9353) (objectui `dab9f96ec`) +- _(no changeset)_ docs(tooling): reserve --rewrite-governed-file by its condition, not by actor (#9383) (objectui `07da32e28`) +- _(no changeset)_ fix(skills): guard the DataSource read in the marked data-integration example (#9352) (objectui `28be0786d`) +- _(no changeset)_ hooks: the three remaining guards name the environment their hatch variable must be set in, never a command prefix (#9300) (objectui `a5921a0f8`) +- _(no changeset)_ fix(ci): put the `scripts/__tests__` markdown population on the shard trigger (#9141) (objectui `4c0dc090a`) +- _(no changeset)_ docs(skills): teach object-nav target exclusivity, not a precedence the spec refuses (#9227) (objectui `bedd7344f`) +- _(no changeset)_ docs(agents): require a runtime reading for inertness claims, and a control for any population-size reading (#9226) (objectui `aebc3a31f`) +- _(no changeset)_ fix(ci): read a locale-catalogue chunk whose content hash contains a hyphen (#9228) (objectui `2102f6125`) +- _(no changeset)_ feat(ci): refuse a merge group whose queued pull request still carries `needs:contract-review` (#9212) (objectui `a94e4d073`) +- _(no changeset)_ docs(scripts): record what a re-baseline absorbs, and that BASELINE.commit cannot be checked from `main` (objectui#7848) (#9208) (objectui `75d34d604`) +- _(no changeset)_ devx(scripts): register check-bash32-floor.mjs in the upstream port pin at its own ref (#9207) (objectui `0f7f8e61c`) +- _(no changeset)_ fix(gate): the expression-carriage blind-spot leg reads the JS object-literal dialect (#9193) (objectui `7696daac0`) +- _(no changeset)_ test(scripts): census why the push-lane coverage gate was red, by cause (#9180) (objectui `87f174c00`) +- _(no changeset)_ fix(gate): make an unrecognised half status LOUD in the eager-closure fold (#9156) (objectui `af674b99a`) +- _(no changeset)_ test(scripts): derive the zero-test workspace members and pin the exclusion so it can expire (objectui#9106) (#9147) (objectui `049f09504`) +- _(no changeset)_ fix(prompts): rule each key-teaching section, and widen check:prompt-keys to read them (#9143) (objectui `44a9b4bd2`) +- _(no changeset)_ docs(changeset): correct six pending changesets whose claims a later merge falsified (#9139) (objectui `36fc71f1c`) +- _(no changeset)_ feat(scripts): derive what each pack-object importer reads off the pack, and how deep (objectui#9046) (#9128) (objectui `c736084bf`) +- _(no changeset)_ fix(docs): stop pricing the eager-closure ruling with a page count nothing derives (#9118) (objectui `567f37019`) +- _(no changeset)_ test(devx): give layout, test-support and console-starter a package-level test entry (#9105) (objectui `58a4fada7`) +- _(no changeset)_ fix(prompts): teach only view keys a real renderer answers, and gate it (#9099) (objectui `d2f0c108c`) +- _(no changeset)_ ci(test): run the shards when a markdown document a test READS changes (#9097) (objectui `a92eef266`) +- _(no changeset)_ fix(scripts): check-side-effects-array walks every published entry point, not just the source barrel (#9084) (objectui `7f3a7ea69`) +- _(no changeset)_ test(ci-docs): pin the Workflow Inventory table, which the inventory test could not see (#9062) (objectui `4ffc333df`) +- _(no changeset)_ docs(changeset): correct two now-false sentences in pending types changesets (#9064) (objectui `a7a818383`) +- _(no changeset)_ fix(scripts): derive the dead-keys pack-object importer population, pin its readings (#9047) (objectui `13372e19d`) +- _(no changeset)_ docs(changeset): drop the stale cardinal from the objectui#8315 changeset (#9023) (objectui `a650bb356`) +- _(no changeset)_ fix(ci): trigger Build Docs on what the site build actually consumes (#9015) (objectui `1e433418b`) +- _(no changeset)_ fix(ci): make pre-install-import-graph.yml point at its population instead of counting it (#8995) (objectui `e8b7b0785`) +- _(no changeset)_ docs(changeset): correct two present-tense claims a later PR falsified (#8994) (objectui `35c6a3453`) +- _(no changeset)_ fix(lint): drop git-ignored build output from ESLint's own walk (#8986) (objectui `403d9efde`) +- _(no changeset)_ docs(setup): point setup.sh's third "Next steps" read at a doc that exists (#8982) (objectui `6112e0dad`) +- _(no changeset)_ test(scripts): census the `$`-dialect lowercase aliases before objectui#8568 is ruled (#8977) (objectui `ca67d42f0`) +- …and 179 more commits with no changeset — this list is capped at 100, the range has 279 in total. Run `node scripts/objectui-range.mjs --from 53ded82bf7a4 --to 87af769e9a3e --all` for the complete list. + + + +objectui range: `53ded82bf7a4...87af769e9a3e` diff --git a/.objectui-sha b/.objectui-sha index a9b031ceea7..035b6937d64 100644 --- a/.objectui-sha +++ b/.objectui-sha @@ -1 +1 @@ -53ded82bf7a494f54e344e19099dbf00854b8694 +87af769e9a3ee28ace099fdd653d3ebd79fe82e2 diff --git a/content/docs/references/ui/dataset.mdx b/content/docs/references/ui/dataset.mdx index 5dab244d1e1..252a4826830 100644 --- a/content/docs/references/ui/dataset.mdx +++ b/content/docs/references/ui/dataset.mdx @@ -84,7 +84,7 @@ const result = DatasetSchema.parse(data); | **aggregate** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | optional | Aggregation (sum/avg/count/...); omit when `derived` is set | | **field** | `string` | optional | Aggregated field; optional for count(*) | | **filter** | `any` | optional | | -| **format** | `string` | optional | Numeral pattern for a NUMERIC measure — grouping, decimals, percent; e.g. "0,0.00", "0.0%". An amount takes its symbol from `currency`, not from a "$" in the pattern. A DATE-valued measure never reads a date pattern: `"YYYY-MM-DD"` renders the locale default. A date-only value reads `format` as a display style (`short`, `relative`); a datetime value ignores it. | +| **format** | `string` | optional | Numeral pattern for a NUMERIC measure — grouping, decimals, percent; e.g. "0,0.00", "0.0%". An amount takes its symbol from `currency`, not from a "$" in the pattern. A DATE-valued measure never reads a date pattern: `"YYYY-MM-DD"` renders that arm's default face. A date or datetime value reads `format` as a display style — `short` or `relative`, honoured on both. | | **currency** | `string` | optional | Display currency code (ISO 4217) | | **derived** | `{ op: Enum<'ratio' \| 'sum' \| 'difference' \| 'product'>; of: string[] }` | optional | | @@ -125,7 +125,7 @@ const result = DatasetSchema.parse(data); | **aggregate** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | optional | Aggregation (sum/avg/count/...); omit when `derived` is set | | **field** | `string` | optional | Aggregated field; optional for count(*) | | **filter** | `any` | optional | | -| **format** | `string` | optional | Numeral pattern for a NUMERIC measure — grouping, decimals, percent; e.g. "0,0.00", "0.0%". An amount takes its symbol from `currency`, not from a "$" in the pattern. A DATE-valued measure never reads a date pattern: `"YYYY-MM-DD"` renders the locale default. A date-only value reads `format` as a display style (`short`, `relative`); a datetime value ignores it. | +| **format** | `string` | optional | Numeral pattern for a NUMERIC measure — grouping, decimals, percent; e.g. "0,0.00", "0.0%". An amount takes its symbol from `currency`, not from a "$" in the pattern. A DATE-valued measure never reads a date pattern: `"YYYY-MM-DD"` renders that arm's default face. A date or datetime value reads `format` as a display style — `short` or `relative`, honoured on both. | | **currency** | `string` | optional | Display currency code (ISO 4217) | | **derived** | `{ op: Enum<'ratio' \| 'sum' \| 'difference' \| 'product'>; of: string[] }` | optional | | diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 295c01638df..4e0d05c3508 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -199,7 +199,7 @@ Column footer summary configuration | **required** | `boolean` | optional | Required override | | **hidden** | `boolean` | optional | Hidden override | | **colSpan** | `integer` | optional | Absolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (`colSpan: 4` in a 3-column grid renders as 3); a `colSpan` within the column count renders as authored, and `colSpan: 1` emits no span class at all. | -| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `53ded82bf7`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin only the widest tier's class is emitted (`@2xl:col-span-3` for a 3-column grid), so at intermediate widths the field takes a single cell, not the row (one of two at the 720px modal width; measured in Chromium at viewport widths 390, 720 and 1700). | +| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `87af769e9`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. | | **widget** | `string` | optional | Custom widget/component name (overrides type-based inference) | | **language** | `string` | optional | Code editor language (for type=code) | | **keyField** | `{ field?: string; label?: string \| Record; placeholder?: string \| Record; helpText?: string \| Record; … }` | optional | Key column config for record-typed fields | @@ -364,7 +364,7 @@ View filter rule | **required** | `boolean` | optional | Required override | | **hidden** | `boolean` | optional | Hidden override | | **colSpan** | `integer` | optional | Absolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (`colSpan: 4` in a 3-column grid renders as 3); a `colSpan` within the column count renders as authored, and `colSpan: 1` emits no span class at all. | -| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `53ded82bf7`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin only the widest tier's class is emitted (`@2xl:col-span-3` for a 3-column grid), so at intermediate widths the field takes a single cell, not the row (one of two at the 720px modal width; measured in Chromium at viewport widths 390, 720 and 1700). | +| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `87af769e9`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. | | **widget** | `string` | optional | Custom widget/component name (overrides type-based inference) | | **language** | `string` | optional | Code editor language (for type=code) | | **keyField** | `{ field?: string; label?: string \| Record; placeholder?: string \| Record; helpText?: string \| Record; … }` | optional | Key column config for record-typed fields | diff --git a/packages/sdui-parser/objectui-lockstep.json b/packages/sdui-parser/objectui-lockstep.json index bc425df35b9..e0a6988b1c9 100644 --- a/packages/sdui-parser/objectui-lockstep.json +++ b/packages/sdui-parser/objectui-lockstep.json @@ -6,21 +6,23 @@ ], "objectui": { "repo": "https://github.com/objectstack-ai/objectui.git", - "rev": "53ded82bf7a494f54e344e19099dbf00854b8694", - "revDate": "2026-09-05T15:42:42+00:00", + "rev": "87af769e9a3ee28ace099fdd653d3ebd79fe82e2", + "revDate": "2026-09-20T15:03:40+00:00", "source": "packages/sdui-parser/src", "files": [ + "packages/sdui-parser/src/body-dialect.ts", "packages/sdui-parser/src/codegen.ts", "packages/sdui-parser/src/dashboard-widget-options.ts", "packages/sdui-parser/src/index.ts", "packages/sdui-parser/src/input-type.ts", + "packages/sdui-parser/src/kanban-quick-add.ts", "packages/sdui-parser/src/parse.ts", "packages/sdui-parser/src/provenance.ts", "packages/sdui-parser/src/types.ts", "packages/sdui-parser/src/validate.ts" ] }, - "recordedAgainstPin": "53ded82bf7a494f54e344e19099dbf00854b8694", + "recordedAgainstPin": "87af769e9a3ee28ace099fdd653d3ebd79fe82e2", "grammarRegion": { "file": "packages/sdui-parser/src/parse.ts", "delimiter": "/* ---------------------- the JS literal subset (#6614) ---------------------- */", @@ -37,7 +39,9 @@ "forbidden-attr", "forbidden-tag", "inert-expression", + "inert-quick-add", "invalid-enum", + "member-type-mismatch", "mismatched-tag", "missing-required-prop", "multiple-roots", diff --git a/packages/sdui-parser/src/__tests__/inert-quick-add.test.ts b/packages/sdui-parser/src/__tests__/inert-quick-add.test.ts new file mode 100644 index 00000000000..723cad21333 --- /dev/null +++ b/packages/sdui-parser/src/__tests__/inert-quick-add.test.ts @@ -0,0 +1,234 @@ +/** + * `quickAdd` on the `ObjectKanbanRenderer` tag is DIAGNOSED, and diagnosed + * truthfully — the objectui#8285 mechanism (ruling of 2026-09-08, batch #91), + * ported into this copy in lockstep (objectstack#17645). + * + * WHY THESE PINS EXIST HERE. Two copies of this parser exist — objectui's + * `packages/sdui-parser` (which the browser RENDERER runs) and this hoisted one + * (which the SAVE GATE runs). These pins are the objectstack half of the + * lockstep for this diagnostic; the ported module is byte-equal to objectui's. + * Before this port, this copy answered an authored `quickAdd` with the generic + * `unknown-prop` while objectui answered `inert-quick-add` — the same authored + * page getting two different readings from the two tiers, which is the dialect + * split the lockstep invariant forbids. + * + * ## What is red here before the change, and what is not + * + * ⚠️ Only the rows that assert `inert-quick-add` are red before this change. + * `quickAdd` is undeclared on the kanban registrations (objectui#8201 escalated + * it rather than declaring it), so this tier ALREADY reported it — as + * `unknown-prop`, a message that is false against the contract + * `@objectstack/spec` still publishes. The port replaces a wrong reading; it + * does not add a first one. + * + * The remaining rows are green in BOTH worlds by construction, and each is kept + * because it names a WRONG FIX that would otherwise pass: + * + * - the unknown-prop control — a fix that suppressed the generic walk for + * this block rather than for this one key; + * - the two RETIRED kanban-ish tags (`kanban`, `kanban-ui`) — a fix scoped + * to "a kanban-ish tag" rather than to the one registration that serves + * `ObjectKanbanRenderer`. Both left objectui's registry (objectui#8802 / + * objectui#8257), so neither is a host any more; they stay declared in the + * hand-built manifest below precisely so this row can still discriminate; + * - `quickAdd: false` — a fix keyed on the KEY's presence rather than on the + * author asking for the control, which would warn about a value that got + * exactly what it asked for; + * - the braced marker — a fix that reads the parser's opaque `$expr` object + * as "truthy, therefore the author asked for it"; + * - `ok` staying true — an escalation to `error`, which the objectui#5709 + * ruling ("no new red gates") and objectui#6614 Q2 both put elsewhere. + */ +import { describe, expect, it } from 'vitest'; +import { + INERT_QUICK_ADD, + QUICK_ADD_HOST_TYPES, + QUICK_ADD_KEY, + compile, + manifestFromConfigs, + validateTree, +} from '../index.js'; +import type { Diagnostic, Manifest } from '../types.js'; + +/** + * The three kanban blocks, carrying the inputs their registrations declared — + * `quickAdd` on NONE of them, which is the state this port leaves untouched. + * `card` is a non-kanban control block. + * + * ⚠️ `kanban` and `kanban-ui` are RETIRED registrations (objectui#8802, + * objectui#8257) and are kept here ON PURPOSE, as the discrimination controls + * below: a manifest is an argument to `validateTree`, so this file can still + * ask what the rule says about a tag the live registry no longer produces — + * and the answer must be "not a host". ⛔ Do not read their presence as a + * claim that either tag resolves; against a manifest built from the live + * registry both answer `unknown-component`. + */ +const manifest: Manifest = manifestFromConfigs([ + { + type: 'object-kanban', + namespace: 'plugin-kanban', + inputs: [ + { name: 'objectName', type: 'string', required: true }, + { name: 'groupBy', type: 'string' }, + ], + }, + { + type: 'kanban', + namespace: 'view', + inputs: [ + { name: 'objectName', type: 'string', required: true }, + { name: 'groupBy', type: 'string' }, + ], + }, + { type: 'kanban-ui', namespace: 'plugin-kanban', inputs: [{ name: 'columns', type: 'array' }] }, + { type: 'card', namespace: 'ui', inputs: [] }, +]); + +const diagnose = (node: Record): Diagnostic[] => + validateTree(node as never, manifest).diagnostics; + +const codesFor = (node: Record, key: string): string[] => + diagnose(node).filter((d) => d.message.includes(`"${key}"`)).map((d) => d.code); + +const HOST_TAGS = [...QUICK_ADD_HOST_TYPES].sort(); + +describe('an authored `quickAdd` is diagnosed on the ObjectKanban tags', () => { + it('the host set is the one surviving ObjectKanbanRenderer tag, and is not empty', () => { + // Anti-vacuity for every row below: an empty set would make the negative + // rows trivially true and the positive rows unreachable. + // + // ⚠️ This row is the ONLY thing in this package that a change to + // `QUICK_ADD_HOST_TYPES` reddens — every other row is `it.each(HOST_TAGS)` + // and re-derives itself from the constant, so a narrowing would otherwise + // just delete cases silently. `kanban` left the set with its registration + // (objectui#8802): `checkKanbanQuickAdd` is reached only from + // `validate.ts`'s prop walk, which runs only for a tag the manifest + // RESOLVED, so a tag no registration produces is answered by + // `unknown-component` one level up and never reaches this module. + expect(HOST_TAGS).toEqual(['object-kanban']); + }); + + it('the stamped literal equals the exported constant', () => { + // objectui stamps `code: INERT_QUICK_ADD`; this copy stamps the inline + // literal, because `check:dispatcher-error-vocabulary` cannot reduce a + // kebab-case constant at a `code:` position (the divergence is documented + // at the stamp, and is the same one `unconsumed-widget-option` carries). + // This row is what keeps the two spellings from drifting apart — and the + // lockstep gate resolves the identifier through this constant, so both + // copies still agree on the same code. + expect(INERT_QUICK_ADD).toBe('inert-quick-add'); + const [diagnostic] = diagnose({ type: 'object-kanban', objectName: 'task', quickAdd: true }); + expect(diagnostic.code).toBe(INERT_QUICK_ADD); + }); + + it.each(HOST_TAGS)('<%s> — an authored `quickAdd: true` draws exactly one warning', (tag) => { + const diagnostics = diagnose({ type: tag, objectName: 'task', quickAdd: true }); + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0].code).toBe(INERT_QUICK_ADD); + expect(diagnostics[0].severity).toBe('warning'); + expect(diagnostics[0].tag).toBe(tag); + }); + + it.each(HOST_TAGS)('<%s> — it REPLACES the false `unknown-prop`, it does not join it', (tag) => { + // The reading this port exists to correct: before it, the only thing this + // tier said about this key was that the block "has no prop quickAdd" — + // false against `ComponentPropsMap['object-kanban']`, which publishes it. + expect(codesFor({ type: tag, objectName: 'task', quickAdd: true }, QUICK_ADD_KEY)).toEqual([ + INERT_QUICK_ADD, + ]); + }); + + it.each(HOST_TAGS)('<%s> — the message names the missing half and where the pair works', (tag) => { + // Not a prose pin: these two tokens are what makes the diagnostic + // ACTIONABLE, and a message that dropped either would send its reader back + // to the contract with no explanation, which is the state being fixed. + const [{ message }] = diagnose({ type: tag, objectName: 'task', quickAdd: true }); + expect(message).toContain('onQuickAdd'); + // ⚠️ The remedy names the COMPONENT, not the `kanban-ui` TAG it used to + // name: objectui#8257 retired that registration, so a page written to the + // old advice would now draw `unknown-component` — an ERROR. `KanbanRenderer` + // is still exported from `@object-ui/plugin-kanban` and still forwards both + // halves by identity, so it is the surviving way to get the pair. + expect(message).toContain('KanbanRenderer'); + }); + + it.each(HOST_TAGS)('<%s> — control: a genuinely unknown prop is still reported', (tag) => { + // Guards a fix that turned the generic walk off for this block instead of + // answering for this one key. Green in both worlds by construction. + expect(codesFor({ type: tag, objectName: 'task', bogusProp: 'x' }, 'bogusProp')).toEqual([ + 'unknown-prop', + ]); + }); + + it.each(HOST_TAGS)( + '<%s> — control: a falsy `quickAdd` asks for no control, so it draws none', + (tag) => { + // `KanbanImpl` gates on `quickAdd && onQuickAdd`: `false` got exactly what + // it wrote and nothing was dropped. Guards a fix keyed on the key's mere + // presence. Green in both worlds — the `unknown-prop` half is the status + // quo this port deliberately leaves alone. + expect(codesFor({ type: tag, objectName: 'task', quickAdd: false }, QUICK_ADD_KEY)).toEqual([ + 'unknown-prop', + ]); + }, + ); + + it.each(HOST_TAGS)( + '<%s> — control: a braced value this tier never evaluates is not read as a request', + (tag) => { + // The parser's deferred marker is an opaque object — truthy in JS, and no + // reading at all about what the author asked for. Guards a fix that tested + // truthiness alone. Green in both worlds. + expect( + codesFor( + { type: tag, objectName: 'task', quickAdd: { $expr: 'rows.length > 0' } }, + QUICK_ADD_KEY, + ), + ).toEqual(['unknown-prop']); + }, + ); + + it.each(['kanban-ui', 'kanban'])( + 'control: `%s` is NOT a host — a kanban-ish tag alone does not arm this diagnostic', + (tag) => { + // Guards a fix scoped to "a kanban-ish tag". Both spellings are RETIRED + // registrations (objectui#8257, objectui#8802), so neither can reach this + // module through a manifest built from the live registry at all — they are + // declared in this file's hand-built manifest so the discrimination is + // still measurable here, which is the one thing a synthetic manifest can + // do that the live one cannot. `kanban-ui`'s pair itself survives on the + // exported `KanbanRenderer` component, which no tag resolves to. + const node = { type: tag, columns: [], quickAdd: true }; + expect(diagnose(node).map((d) => d.code)).not.toContain(INERT_QUICK_ADD); + expect(codesFor(node, QUICK_ADD_KEY)).toEqual(['unknown-prop']); + }, + ); + + it('control: a non-kanban block is untouched', () => { + expect(diagnose({ type: 'card', quickAdd: true }).map((d) => d.code)).toEqual(['unknown-prop']); + }); + + it('through the whole pipeline: the diagnostic survives parse + validate on real source', () => { + // A SUBJECT row, not a control — it is red without the port, like the rows + // above. It is here because every row above hands `validateTree` a + // hand-built node: this one starts from source text, so it also proves the + // parser really materializes the braced `true` as a boolean rather than as + // the `$expr` marker the falsy/braced controls are about. + const { diagnostics } = compile('', manifest); + expect(diagnostics.map((d) => d.code)).toEqual([INERT_QUICK_ADD]); + }); + + it('control: the page still COMPILES — a warning, not a new red gate', () => { + // `ok` is `!diagnostics.some(d => d.severity === 'error')`, i.e. the save + // gate's pass/fail, and it is true in BOTH worlds: `unknown-prop` was a + // warning too, so nothing hardens here. Kept because it names the wrong + // fix — escalating an inert authored key to `error`, which objectui#5709 + // ("no new red gates") and objectui#6614 Q2 both put elsewhere, and which + // would stop a page that saves today from saving. + expect(compile('', manifest).ok).toBe(true); + // Non-vacuity for that `true`: the same pipeline DOES turn `ok` false when + // an error-severity diagnostic is present, so this is a reading about this + // diagnostic's severity and not about `ok` being unreachable. + expect(compile('', manifest).ok).toBe(false); + }); +}); diff --git a/packages/sdui-parser/src/__tests__/member-type-mismatch.test.ts b/packages/sdui-parser/src/__tests__/member-type-mismatch.test.ts new file mode 100644 index 00000000000..4b87cadaa06 --- /dev/null +++ b/packages/sdui-parser/src/__tests__/member-type-mismatch.test.ts @@ -0,0 +1,208 @@ +/** + * `ManifestInput.of`, the coarse MEMBER kind — the objectui#8067 mechanism, + * ported into this copy in lockstep (objectstack#17645). + * + * WHY THESE PINS EXIST HERE. Two copies of this parser exist — objectui's + * `packages/sdui-parser` (which the browser RENDERER runs) and this hoisted one + * (which the SAVE GATE runs). These pins are the objectstack half of the + * lockstep for the member check; the ported functions are byte-equal to + * objectui's. Before this port `of` did not exist in this copy at all: a member + * that drifted from the contract was invisible here while objectui reported it, + * so the same authored page produced a diagnostic on one surface and silence on + * the other — the dialect split the lockstep invariant forbids (both copies + * agree on the accepted grammar AND on diagnostic codes). + * + * WHY THE KEY EXISTS. `type: 'array'` said a value was a list and stopped + * there, so a member that drifted from the contract was invisible to every + * layer that reads a declaration. `page:header.actions` is the measured cost: + * spec `z.array(z.string())` ("Action IDs"), a renderer reading the members as + * `ActionDef` OBJECTS, and the repo-wide parity gate green for the whole life of + * the drift because both sides had the key and neither could say what was + * inside it. + * + * WHAT IS PINNED HERE, and they are different facts: + * + * 1. THE READER EXISTS — a member no declared arm accepts is REPORTED. A key + * that nothing reads is a declared-but-inert key with a new name, which is + * the defect this repo's enforce-or-remove rule exists to refuse. + * 2. BACKWARD COMPATIBILITY — an input that declares no `of` is validated, + * serialized and typed byte-identically to before the key existed. That is + * what makes this an extension of `sdui.manifest.json` rather than a new + * version of it. + * 3. THE COARSE CEILING HOLDS ONE LEVEL DOWN — `of` names a KIND, never a + * domain and never a member's KEYS, so `of: 'object'` clears every object + * whatever it contains. The maintainer ruling of 2026-08-17 ("SPEC IS THE + * SOLE JUDGE OF VALUES") is untouched. + * 4. THE OTHER CONSUMERS — the serializer canonicalizes `of` exactly as it + * canonicalizes `type`, and the JSX codegen narrows the emitted element + * type from it. A member kind the validator honours but the `.d.ts` + * contradicts would just move an author's false error one layer over. + */ +import { describe, expect, it } from 'vitest'; +import { generateDts, manifestFromConfigs, validateTree } from '../index.js'; +import type { Manifest, SchemaElement } from '../types.js'; + +const one = (inputs: Parameters[0][number]['inputs']): Manifest => + manifestFromConfigs([{ type: 'probe', namespace: 'ui', inputs }]); + +const diags = (manifest: Manifest, node: Record) => + validateTree({ type: 'probe', ...node } as SchemaElement, manifest).diagnostics; + +const codes = (manifest: Manifest, node: Record): string[] => + diags(manifest, node).map((d) => d.code); + +describe('a declared member kind is READ', () => { + const manifest = one([{ name: 'actions', type: 'array', of: 'string' }]); + + it('clears an array whose members are all of the declared kind', () => { + expect(codes(manifest, { actions: ['clone', 'convert'] })).toEqual([]); + }); + + it('reports a member of the wrong kind — the drift this key exists to catch', () => { + // Verbatim the `page:header.actions` drift: spec says action IDs, the value + // carries `ActionDef` objects. Before `of` this node was clean. + expect(codes(manifest, { actions: [{ name: 'clone' }] })).toEqual(['member-type-mismatch']); + }); + + it('names every offending position in ONE diagnostic, not one per member', () => { + const [diagnostic] = diags(manifest, { actions: ['clone', 42, {}, 'convert'] }); + expect(diagnostic.code).toBe('member-type-mismatch'); + expect(diagnostic.severity).toBe('warning'); + expect(diagnostic.message).toBe( + ' prop "actions" expected every member to be a string — [1], [2] are not', + ); + }); + + it('an empty container conforms — there is no member to refuse', () => { + expect(codes(manifest, { actions: [] })).toEqual([]); + }); + + it('reads the ARRAY form of `of` the same way it reads the array form of `type`', () => { + // The rot this pins: a reader that forgot `Array.isArray` would fall through + // and report NOTHING, which looks exactly like a clean value. + const union = one([{ name: 'items', type: 'array', of: ['string', 'object'] }]); + expect(codes(union, { items: ['a', { b: 1 }] })).toEqual([]); + expect(codes(union, { items: [42] })).toEqual(['member-type-mismatch']); + }); + + it('judges an OBJECT container by its VALUES — the map half of the key', () => { + const map = one([{ name: 'labels', type: 'object', of: 'string' }]); + expect(codes(map, { labels: { en: 'Account', 'zh-CN': '客户' } })).toEqual([]); + const [diagnostic] = diags(map, { labels: { en: 'Account', count: 3 } }); + expect(diagnostic.message).toBe( + ' prop "labels" expected every member to be a string — [count] is not', + ); + }); +}); + +describe('the container verdict comes first', () => { + const manifest = one([{ name: 'actions', type: 'array', of: 'string' }]); + + it('a wrong CONTAINER draws one diagnostic, not two', () => { + // One mistake, one report. A member walk over a value that is not even the + // declared container would name positions of a shape the author never wrote. + expect(codes(manifest, { actions: 'clone' })).toEqual(['type-mismatch']); + }); + + it('a value that satisfied a NON-container arm of a union is not member-judged', () => { + const union = one([{ name: 'actions', type: ['string', 'array'], of: 'string' }]); + expect(codes(union, { actions: 'clone' })).toEqual([]); + expect(codes(union, { actions: [42] })).toEqual(['member-type-mismatch']); + }); +}); + +describe('the coarse ceiling holds one level down', () => { + it("`of: 'object'` clears every object, whatever keys it carries", () => { + // `of` is a KIND, never a member's KEYS. Which keys an element must have is + // spec's question and a per-block pin's, exactly as the value DOMAIN of a + // `number` arm is. + const manifest = one([{ name: 'sections', type: 'array', of: 'object' }]); + expect(codes(manifest, { sections: [{ anything: 'at all' }, {}] })).toEqual([]); + expect(codes(manifest, { sections: ['sales_info'] })).toEqual(['member-type-mismatch']); + }); + + it('an `enum` member arm raises its severity to error, as it does one level up', () => { + const manifest = one([ + { name: 'types', type: 'array', of: 'enum', enum: ['comment', 'task'] }, + ]); + expect(codes(manifest, { types: ['comment', 'task'] })).toEqual([]); + const [diagnostic] = diags(manifest, { types: ['comment', 'email'] }); + expect(diagnostic.code).toBe('member-type-mismatch'); + expect(diagnostic.severity).toBe('error'); + }); +}); + +describe('an input that declares no member kind is unchanged', () => { + // The backward-compatibility half. Every input published before this key + // existed says exactly this, so anything that moves here moves for all of + // them — including every entry of the tracked `sdui.manifest.json`. + const manifest = one([{ name: 'actions', type: 'array' }]); + + it('draws no member diagnostic on any member', () => { + expect(codes(manifest, { actions: [42, {}, 'clone', null] })).toEqual([]); + }); + + it('publishes no `of` at all — the serialized entry is byte-identical', () => { + expect(JSON.stringify(manifest.components.probe.inputs[0])).toBe( + '{"name":"actions","type":"array"}', + ); + }); + + it('emits the unnarrowed element type', () => { + expect(generateDts(manifest)).toContain('actions?: unknown[];'); + }); +}); + +describe('the serializer canonicalizes `of` exactly as it canonicalizes `type`', () => { + it('collapses a one-element array to the bare kind', () => { + expect(one([{ name: 'a', type: 'array', of: ['string'] }]).components.probe.inputs[0].of).toBe( + 'string', + ); + }); + + it('drops an off-vocabulary arm rather than inventing `string` for it', () => { + expect( + one([{ name: 'a', type: 'array', of: ['string', 'nonsense'] }]).components.probe.inputs[0].of, + ).toBe('string'); + }); + + it('dedupes a repeated arm', () => { + expect( + one([{ name: 'a', type: 'array', of: ['string', 'object', 'string'] }]).components.probe + .inputs[0].of, + ).toEqual(['string', 'object']); + }); + + it('does NOT invent a member kind for an undeclared `of`', () => { + // `canonicalizeInputType`'s no-arms fallback is `'string'`, so routing an + // undefined through it would make every array in every manifest claim + // string members it was never told it had. + expect(one([{ name: 'a', type: 'array' }]).components.probe.inputs[0].of).toBeUndefined(); + }); +}); + +describe('the JSX authoring surface narrows with it', () => { + it('types the elements rather than emitting `unknown[]`', () => { + const dts = generateDts(one([{ name: 'actions', type: 'array', of: 'string' }])); + expect(dts).toContain('actions?: string[];'); + expect(dts).not.toContain('actions?: unknown[];'); + }); + + it("types an object map's values", () => { + expect(generateDts(one([{ name: 'labels', type: 'object', of: 'string' }]))).toContain( + 'labels?: Record;', + ); + }); + + it('emits a parenthesised union for a multi-arm member declaration', () => { + expect(generateDts(one([{ name: 'items', type: 'array', of: ['string', 'object'] }]))).toContain( + 'items?: (string | Record)[];', + ); + }); + + it('a `slot`-only member declaration types no member — never a silent `string`', () => { + expect(generateDts(one([{ name: 'items', type: 'array', of: 'slot' }]))).toContain( + 'items?: unknown[];', + ); + }); +}); diff --git a/packages/sdui-parser/src/codegen.ts b/packages/sdui-parser/src/codegen.ts index 1f3c9acb2e8..76a7d654f1f 100644 --- a/packages/sdui-parser/src/codegen.ts +++ b/packages/sdui-parser/src/codegen.ts @@ -101,9 +101,9 @@ function armTsType(arm: ManifestInputType, input: ManifestInput): string { case 'boolean': return 'boolean'; case 'array': - return 'unknown[]'; + return `${memberTsType(input)}[]`; case 'object': - return 'Record'; + return `Record`; case 'enum': { const vals = (input.enum ?? []).map((e) => (typeof e === 'object' ? e.value : e)); return vals.length ? vals.map((v) => JSON.stringify(v)).join(' | ') : 'string'; @@ -113,6 +113,29 @@ function armTsType(arm: ManifestInputType, input: ManifestInput): string { } } +/** + * The TypeScript type of a container's MEMBERS, from `ManifestInput.of` + * (objectui#8067). + * + * `unknown` when nothing is declared, which is what every input published + * before `of` existed says — so an undeclared array still emits `unknown[]` and + * an undeclared object still emits `Record`, byte for byte. + * A declared member kind narrows it: `of: 'string'` on `page:header.actions` + * turns `unknown[]` into `string[]`, and the JSX surface finally types the + * action IDs the contract has always required. + * + * `'slot'` is dropped for the same reason {@link valueArms} drops it one level + * up: it names a child position, not a value, so it contributes no member type. + * If that leaves nothing, the result is `unknown` — a declaration that types no + * member must not silently type them all as strings. + */ +function memberTsType(input: ManifestInput): string { + const arms = inputTypeArms(input.of).filter((arm) => arm !== 'slot'); + if (arms.length === 0) return 'unknown'; + const emitted = [...new Set(arms.map((arm) => armTsType(arm, { ...input, of: undefined })))]; + return emitted.length === 1 ? emitted[0] : `(${emitted.join(' | ')})`; +} + /** * A union declaration emits a TypeScript union, so the `.d.ts` an author * type-checks their page against accepts exactly the arms the manifest gate diff --git a/packages/sdui-parser/src/index.ts b/packages/sdui-parser/src/index.ts index 97b94de7642..2ea35795b9c 100644 --- a/packages/sdui-parser/src/index.ts +++ b/packages/sdui-parser/src/index.ts @@ -15,6 +15,12 @@ export { DASHBOARD_WIDGET_HOST_TYPES, UNCONSUMED_WIDGET_OPTION, } from './dashboard-widget-options.js'; +export { + checkKanbanQuickAdd, + INERT_QUICK_ADD, + QUICK_ADD_HOST_TYPES, + QUICK_ADD_KEY, +} from './kanban-quick-add.js'; export { generateDts, propsName, generateBlockList } from './codegen.js'; export type { CodegenOptions } from './codegen.js'; export { inputTypeArms, canonicalizeInputType, MANIFEST_INPUT_TYPES } from './input-type.js'; @@ -76,6 +82,14 @@ export interface RegistryConfigLike { * `canonicalizeInputType` on the way in. */ type: string | string[]; + /** + * The declared member kind(s) — array elements, or an object map's values + * (objectui#8067). Typed as loosely as `type` above and for the same + * reason: this interface is the STRUCTURAL boundary that keeps the package + * free of a registry dependency, so an off-vocabulary value has to be + * representable here and is normalized on the way in. + */ + of?: string | string[]; required?: boolean; enum?: Array; /** @@ -120,6 +134,14 @@ export function manifestFromConfigs( inputs: (c.inputs ?? []).map((i) => ({ name: i.name, type: canonicalizeInputType(i.type), + // Undefined stays undefined rather than going through + // `canonicalizeInputType`, whose no-arms fallback is `'string'`: an + // input that declares no member kind must publish NO `of`, or every + // array in every manifest would suddenly claim string members it was + // never told it had. `JSON.stringify` drops the undefined key, so the + // published artifact is byte-identical for every input that does not + // declare one (objectui#8067). + of: i.of === undefined ? undefined : canonicalizeInputType(i.of), required: i.required, enum: i.enum, binding: i.binding, diff --git a/packages/sdui-parser/src/kanban-quick-add.ts b/packages/sdui-parser/src/kanban-quick-add.ts new file mode 100644 index 00000000000..20f9f74b9f0 --- /dev/null +++ b/packages/sdui-parser/src/kanban-quick-add.ts @@ -0,0 +1,177 @@ +/** + * The inert `quickAdd` on the `ObjectKanbanRenderer` tag (objectui#8285, + * director-seat ruling of 2026-09-08, decision batch #91) + * + * ## WHY THIS MODULE EXISTS HERE + * + * Two copies of this parser exist: objectui's `packages/sdui-parser` (which the + * browser RENDERER runs) and this one (which the SAVE GATE runs). Everything + * from the `import` line below to end of file is the port of objectui's copy. + * The two copies must agree on the accepted grammar AND on diagnostic codes — + * if they drift, the save gate and the renderer speak different dialects and a + * page can save clean and render inert. Change this only together with the + * objectui copy, and re-record `pnpm gen:sdui-lockstep` when you do. + * + * ## What is wrong, measured rather than argued + * + * `@objectstack/spec`'s `ComponentPropsMap['object-kanban']` publishes + * `quickAdd: z.boolean().optional()`, and the board does not honour it. + * `KanbanImpl` gates the Quick Add control on BOTH halves of a pair — + * `quickAdd && onQuickAdd`, at the in-column button and again at + * `QuickAddForm` — and `onQuickAdd` is a RUNTIME SLOT: a host-supplied + * FUNCTION, refused by name in `complex.zod.ts` because JSON has no function + * value. `ObjectKanban` substitutes its own `onCardMove` and `onCardClick` and + * stops there; it supplies no `onQuickAdd`. + * + * On THIS tier the second half is not merely absent, it is unwritable: a + * handler attribute on a parsed page draws `forbidden-attr`, because this tier + * parses and never executes. So `quickAdd` is inert for every author who can + * reach it here — there is no out-of-band consumer to protect, which is the + * fact that makes the diagnostic below unconditional rather than escape-hatched + * the way `unconsumed-widget-option` is. + * + * ## What this replaces, and why the previous reading was WRONG rather than absent + * + * `quickAdd` is deliberately absent from the kanban registrations' declared + * inputs (objectui#8201 escalated it rather than declaring it), so + * `validateTree` already reported it — as `unknown-prop`, "has no prop + * quickAdd". + * + * That message is a MISDIAGNOSIS. objectui#8201's pin header records the same + * shape from the other direction: a tier telling an author that spellings which + * WORK are unknown "trains authors (AI authors included) to delete working + * metadata". Here the falsehood runs the other way and is worse for it — the + * spec DOES publish this prop, so an author who checks the contract finds the + * warning contradicted, concludes the manifest is behind the spec, and keeps a + * key that will never do anything. It also cannot be told apart from a typo, so + * nothing anywhere records WHY the key is refused. + * + * This module says the true thing instead: the pair, the missing half, and + * where the pair does work. + * + * ## Severity — WARNING, and the escalation is deliberately not taken + * + * The ruling's word is "diagnosed", not "refused", and every diagnostic in this + * family is a warning: `unknown-prop` (what this replaces, so nothing hardens), + * `inert-expression`, and `unconsumed-widget-option`, whose objectui#5709 + * ruling states the constraint directly — "no gate weakening and no new red + * gates were ruled". `validate.ts`'s `inert-expression` note records that + * escalating an inert authored key to `error` is a SEPARATE question + * (objectui#6614 Q2) belonging at the save gate, and it is not answered here: + * an error would stop a page that saves today from saving, which is a cost, and + * costs are the maintainer's to impose. + * + * The contract-side refusal is where the hardening belongs, and it is already + * ruled: option B retires `object-kanban.quickAdd` from the spec's + * `ComponentPropsMap` (ADR-0049 enforce-or-remove), after which a strict parse + * refuses the key BY NAME and this module is deleted along with the interim it + * names. ⚠️ THIS FILE IS THAT INTERIM. + * + * ## Scope — the three places this deliberately does not fire + * + * - `KanbanRenderer`, the component. The ruling keeps the `quickAdd` / + * `onQuickAdd` pair there untouched: it forwards both halves by identity, + * and a React host mounting it CAN pass the function. ⚠️ It is no longer + * reachable as a TAG — objectui#8257 retired the `kanban-ui` registration, + * so `` is now an `unknown-component` ERROR, not a block with a + * working pair. The component stays exported from + * `@object-ui/plugin-kanban`, which is the surviving way to get the pair, + * and is what this module's message names. + * - `quickAdd: false`, and any other falsy value. The renderer's own gate is + * `quickAdd && onQuickAdd`, so a falsy value asks for no control and gets + * none — the author got what they wrote, and nothing was dropped. The + * ruling names `quickAdd: true`. + * - A braced value this tier never evaluates (the parser's `$expr` marker). + * It is opaque here, so "the author asked for the control" is not a reading + * this module can make; the value's own inertness is `inert-expression`'s + * subject, not this one's. + */ +import type { Diagnostic } from './types.js'; + +/** The diagnostic `code` this module emits. */ +export const INERT_QUICK_ADD = 'inert-quick-add'; + +/** The authored key it is about. */ +export const QUICK_ADD_KEY = 'quickAdd'; + +/** + * The tags served by `ObjectKanbanRenderer` — the registrations in objectui's + * `packages/plugin-kanban/src/index.tsx`, which is where another one would + * appear. Restated here as data because this package is deliberately free of + * any dependency on the registry or on a plugin (see `RegistryConfigLike` in + * `index.ts`). + * + * ⚠️ **`kanban` left this set with its registration** (objectui#8802, ruled + * 2026-09-09; `kanban-ui` and `kanban-enhanced` went the same way under + * objectui#8257). Keeping it would have been dead data, MEASURED and not + * assumed: `checkKanbanQuickAdd` has exactly one call site, inside + * `validate.ts`'s prop walk, and that walk runs only in the branch where + * `manifest.components[node.type]` RESOLVED. A tag no registration produces is + * answered one level up, by `unknown-component`, and its props are never walked + * at all — so on a manifest built from the live registry a `` + * node draws `error/unknown-component` and nothing else, against a firing + * control on `` that draws `warning/inert-quick-add`. + * ⛔ Nothing is silently dropped by the narrowing: the retired spelling is + * refused BY NAME at the tag, which is a louder answer than this warning, and + * stacking both would be the two-diagnostics-for-one-mistake shape + * `checkMemberTypes` already refuses (objectui#8067). The one path that could + * still reach a `kanban` entry is a HAND-BUILT manifest declaring a component + * of that name — which, after the retirement, is somebody else's component, and + * the message below asserts things about `ObjectKanban` that would be false of + * it. + */ +export const QUICK_ADD_HOST_TYPES: ReadonlySet = new Set(['object-kanban']); + +const isPlainObject = (v: unknown): v is Record => + typeof v === 'object' && v !== null && !Array.isArray(v); + +/** The parser's deferred-expression marker — opaque here, never evaluated. */ +const isExpr = (v: unknown): boolean => isPlainObject(v) && '$expr' in v; + +/** + * The diagnostic for ONE authored prop, or `null` when this module has nothing + * to say about it. + * + * Per-prop rather than per-node — unlike `checkDashboardWidgetOptions`, which + * runs after the prop walk — because this diagnostic REPLACES the one the walk + * would otherwise emit for the same key. Two diagnostics for one mistake is the + * shape `validate.ts` already refuses at `checkMemberTypes` (objectui#8067). + * + * Deliberately independent of whether the registration DECLARES the key: the + * claim is about the render path, which a declaration cannot change. Reading it + * out of the `!input` branch would make declaring `quickAdd` silently disarm + * this — the one edit that must instead leave the warning standing. + */ +export function checkKanbanQuickAdd(tag: string, key: string, value: unknown): Diagnostic | null { + if (key !== QUICK_ADD_KEY) return null; + if (!QUICK_ADD_HOST_TYPES.has(tag)) return null; + if (isExpr(value)) return null; + if (!value) return null; + return { + severity: 'warning', + // DIVERGENCE FROM OBJECTUI, and the only one below this file's header: + // objectui writes `code: INERT_QUICK_ADD` here. This repo runs + // `check:dispatcher-error-vocabulary`, whose `objlitconst` shape reads the + // SCREAMING_SNAKE constant NAME at a `code:` position and then must reduce + // it to a literal — and its literal grammar is `[A-Za-z][A-Za-z0-9_]*`, + // which a KEBAB-case value cannot satisfy. So the constant form is reported + // as an unresolvable code constant, and that finding cannot be declared + // away. `inert-quick-add` is a parser DIAGNOSTIC code, not an ADR-0112 wire + // code, and an inline quoted literal is the form both vocabulary gates + // already accept for the sibling diagnostic codes in `validate.ts` and for + // `unconsumed-widget-option` next door. The emitted VALUE is unchanged, and + // the test next door pins it equal to `INERT_QUICK_ADD` so the two + // spellings cannot drift apart. ⛔ This does NOT weaken the lockstep: that + // gate resolves an identifier at a code position through the package's own + // module-level constants, so both spellings reduce to the same code and the + // two copies still agree on all 26. + code: 'inert-quick-add', + message: + `<${tag}> prop "${QUICK_ADD_KEY}" reaches no control — the Quick Add button is gated on ` + + `BOTH "${QUICK_ADD_KEY}" and an "onQuickAdd" handler, and this block supplies neither half ` + + `of the pair: "onQuickAdd" takes a FUNCTION, which no page on this tier can write (this tier ` + + `parses, never executes) and which this board substitutes none of its own for. Drop the key, ` + + `or mount KanbanRenderer from "@object-ui/plugin-kanban" in a React host that passes "onQuickAdd".`, + tag, + }; +} diff --git a/packages/sdui-parser/src/types.ts b/packages/sdui-parser/src/types.ts index 3a7fc45dac0..567bc1259be 100644 --- a/packages/sdui-parser/src/types.ts +++ b/packages/sdui-parser/src/types.ts @@ -77,6 +77,27 @@ export interface ManifestInput { * declared and every already-published entry serializes byte-identically. */ type: ManifestInputType | ManifestInputType[]; + /** + * The coarse kind of the input's MEMBERS — array elements, or the values of + * an object used as a map — as ONE kind or an ARRAY of kinds for a member + * contract that is a union (objectui#8067). + * + * Absent means "not declared", which is what every input published before + * this key existed says: {@link validateTree} checks no member and the + * codegen emits the unnarrowed element type, exactly as before. So a + * manifest gains this key only where a member kind was really declared, and + * every already-published entry serializes byte-identically. + * + * Read the arms through `inputTypeArms(input.of)` — the same accessor + * `type`'s arms go through, since the two fields carry the same shape and a + * reader that forgets the array form is silently inert on it. + * + * LOCKSTEP: this key and its three readers — `validateTree`'s member check, + * `manifestFromConfigs`' canonicalization and the codegen's element type — + * are the port of objectui's copy. The two copies must agree on the accepted + * grammar AND on diagnostic codes; change this only together with objectui. + */ + of?: ManifestInputType | ManifestInputType[]; required?: boolean; /** allowed values for `enum` inputs */ enum?: Array; diff --git a/packages/sdui-parser/src/validate.ts b/packages/sdui-parser/src/validate.ts index 150ccdb824e..156a264073f 100644 --- a/packages/sdui-parser/src/validate.ts +++ b/packages/sdui-parser/src/validate.ts @@ -18,6 +18,7 @@ import type { } from './types.js'; import { inputTypeArms } from './input-type.js'; import { checkDashboardWidgetOptions } from './dashboard-widget-options.js'; +import { checkKanbanQuickAdd } from './kanban-quick-add.js'; /** Base props every node may carry (mirrors BaseSchema) — never "unknown prop". */ const BASE_PROPS = new Set([ @@ -69,6 +70,26 @@ export function validateTree(tree: SchemaElement | null, manifest: Manifest): Va // each provided prop for (const [key, value] of Object.entries(node)) { if (BASE_PROPS.has(key)) continue; + // The `object-kanban` Quick Add pair (objectui#8285): a key + // `@objectstack/spec` still publishes and the renderer cannot honour, + // because the control is gated on a RUNTIME SLOT no parsed page can + // write. It REPLACES whatever the rules below would say about the key — + // `unknown-prop` today, and a coarse type check if the key were ever + // declared — because two diagnostics for one mistake is what + // `checkMemberTypes` already refuses (objectui#8067), and because + // "has no prop quickAdd" is FALSE against the published contract. Asked + // AHEAD of the declaration lookup on purpose: the claim is about the + // render path, so declaring the key must not silently disarm it. + // Interim, by the ruling — the spec's refusal by name replaces it. + // + // LOCKSTEP: this call site and the module behind it are the port of + // objectui's copy. The two copies must agree on the accepted grammar + // AND on diagnostic codes — change this only together with objectui. + const quickAdd = checkKanbanQuickAdd(node.type, key, value); + if (quickAdd) { + diagnostics.push(quickAdd); + continue; + } const input = byName.get(key); if (!input) { diagnostics.push({ @@ -130,7 +151,16 @@ export function validateTree(tree: SchemaElement | null, manifest: Manifest): Va }); } else { const typeDiag = checkType(node.type, input, value); - if (typeDiag) diagnostics.push(typeDiag); + if (typeDiag) { + diagnostics.push(typeDiag); + } else { + // Members only once the CONTAINER kind was accepted. Reporting a + // member of a value that is not even the declared container is two + // diagnostics for one mistake, and the second one names positions + // of a shape the author did not write (objectui#8067). + const memberDiag = checkMemberTypes(node.type, input, value); + if (memberDiag) diagnostics.push(memberDiag); + } } } @@ -168,7 +198,8 @@ export function validateTree(tree: SchemaElement | null, manifest: Manifest): Va /* LOCKSTEP: everything below this line is the byte-equal port of objectui's * `packages/sdui-parser` coarse type check (objectui#3832 — union-typed inputs - * are checked over their arms). The two copies must agree on the accepted + * are checked over their arms — and objectui#8067, the same check one level + * down over the member kind `of` declares). The two copies must agree on the accepted * grammar AND on diagnostic codes/severities — if they drift, the save gate * and the renderer speak different dialects. Change these functions only * together with the objectui copy. */ @@ -226,6 +257,62 @@ function armExpectation(arm: ManifestInputType, input: ManifestInput): string { } } +/** + * The member positions of a container value, as `[position, member]` pairs, or + * `null` when the value has no member position to speak of. + * + * Arrays index by position and objects by key, which is exactly the pair + * `ManifestInput.of` describes: array ELEMENTS, and the VALUES of an object + * used as a map. A scalar returns `null` rather than an empty list, so a value + * that only satisfied a non-container arm of a union declaration + * (`type: ['string', 'array'], of: 'string'`) is not reported as an empty + * container that trivially conforms — it is simply not the arm `of` describes. + */ +function memberEntries(value: unknown): Array<[string, unknown]> | null { + if (Array.isArray(value)) return value.map((member, index) => [String(index), member]); + if (typeof value === 'object' && value !== null) return Object.entries(value); + return null; +} + +/** + * Coarse MEMBER check, over the arms `of` declares (objectui#8067). + * + * The same question `checkType` asks, one level down and with the same answer + * shape: ANY declared arm accepting a member clears it, a member no arm accepts + * is reported, and an input that declares no `of` is checked exactly as it was + * before the key existed — this function returns immediately on an empty arm + * list, so nothing published today changes severity or gains a diagnostic. + * + * ONE diagnostic per prop, naming every offending position, rather than one per + * member: a page that passes an array of the wrong member kind is one mistake + * made once, and N copies of it is the noise this repo treats as the thing that + * trains authors to dismiss real reports. + * + * Severity mirrors `checkType`'s rule for the same reason — `error` when an + * `enum` arm is present, because a closed list is the one fact this layer can + * be certain about; `warning` otherwise, since the coarse kind is a KIND claim + * and `os validate` / `os build` remain the judge of values. + */ +function checkMemberTypes(tag: string, input: ManifestInput, value: unknown): Diagnostic | null { + const arms = inputTypeArms(input.of); + if (arms.length === 0) return null; + const entries = memberEntries(value); + if (entries === null) return null; + const offenders = entries.filter( + ([, member]) => !arms.some((arm) => armAccepts(arm, input, member)), + ); + if (offenders.length === 0) return null; + const expectation = arms.map((arm) => armExpectation(arm, input)).join(' or '); + return { + severity: arms.includes('enum') ? 'error' : 'warning', + code: 'member-type-mismatch', + message: `<${tag}> prop "${input.name}" expected every member to be ${expectation}` + + ` — ${offenders.map(([position]) => `[${position}]`).join(', ')} ` + + `${offenders.length === 1 ? 'is' : 'are'} not`, + tag, + }; +} + /** * Coarse type check, over the arms an input declares (objectui#3832). * diff --git a/packages/spec/src/data/api-methods-batch-conformance.test.ts b/packages/spec/src/data/api-methods-batch-conformance.test.ts index 8fb5116f3bc..2d6ef49ec85 100644 --- a/packages/spec/src/data/api-methods-batch-conformance.test.ts +++ b/packages/spec/src/data/api-methods-batch-conformance.test.ts @@ -61,25 +61,30 @@ const SINGLE_RECORD_WRITE_ONLY: Record = { // `revoked` on ONE key. The multi-select surface this rule protects does not // exist for API keys, and the shape a future one would take does not need // `bulk` either — both read off the console build this release pins - // (`.objectui-sha` = `53ded82bf`, `packages/plugin-grid`; re-measured at - // that pin, 2026-09-08 — previously measured at `a472b0716`, `00d3f09c5`, - // `67dadd602`, before that at `d8ec8d6d4`, `9602dc820`, `190fbd01d`, - // `9a3daf8d3`, originally at `6314e87f2`. `ObjectGrid.tsx` DID change again - // across the move off `a472b0716` (24 insertions, 30 deletions), so the - // selection block this record means was re-READ rather than carried: it is - // `ObjectGrid.tsx:3538-3553` here, was `3544-3559` at `a472b0716`, and the - // sixteen lines are BYTE-IDENTICAL across the hop — `git hash-object` on both - // spans returns `6133933199230670e29d8c7f51c558d86a0af1d2`, so the block only - // shifted six lines UP and none of its substance moved. The earlier hop off + // (`.objectui-sha` = `87af769e9`, `packages/plugin-grid`; re-measured at + // that pin, 2026-09-20 — previously measured at `53ded82bf`, `a472b0716`, + // `00d3f09c5`, `67dadd602`, before that at `d8ec8d6d4`, `9602dc820`, + // `190fbd01d`, `9a3daf8d3`, originally at `6314e87f2`. `ObjectGrid.tsx` DID + // change again across the move off `53ded82bf` (721 insertions, 159 + // deletions), so the selection block this record means was re-READ rather + // than carried: it is `ObjectGrid.tsx:3940-3955` here, was `3538-3553` at + // `53ded82bf` and `3544-3559` at `a472b0716`, and the sixteen lines are + // BYTE-IDENTICAL across the hop — `git hash-object` on both spans returns + // `6133933199230670e29d8c7f51c558d86a0af1d2`, the same id the previous two + // hops recorded, so the block only shifted 402 lines DOWN and none of its + // substance moved. The earlier hop off // `00d3f09c5` is the one that caught the previous record's OWN grid anchor as // wrong rather than merely shifted: `3790-3805` there is // `runBulkActionAggregate` and says nothing about selection. That is the // #10274 class, and the reason a citation refresh re-READS instead of moving // numbers — arithmetic on a wrong anchor produces a fresh-looking span still // describing the wrong function. The second claim, - // `hooks/useBulkExecutor.ts:284-289`, is in a file byte-identical at both - // pins and re-READ there rather than carried on that identity — it still - // ends on `label = 'bulk delete'`, the line the `284-288` span cited five + // `hooks/useBulkExecutor.ts:298-303`, sits in a file that is NO LONGER + // byte-identical across this hop (36 insertions, 22 deletions), so it was + // re-READ rather than carried on file identity: the six lines are themselves + // byte-identical (`git hash-object` on both spans returns + // `01083348330f10a201cdf1078b4c21c236402b6a`) and still end on + // `label = 'bulk delete'`, the line the `284-288` span cited six // pins ago stopped short of, truncating the second of the two branches it // names (byte-identity is never taken as proof an anchor is right): // diff --git a/packages/spec/src/migrations/entries/semantic/18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts index 954f668f653..a3fb09dad88 100644 --- a/packages/spec/src/migrations/entries/semantic/18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts @@ -45,10 +45,10 @@ export const entry: SemanticMigration = { + 'the audience that does not parse. Measured on 884e8347d: the only in-repo readers are ' + 'packages/core/src/health-monitor.ts and packages/core/src/hot-reload.ts, both moved in ' + 'this same change; and the pinned objectui checkout — the pin this repo builds ' - + 'against, `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` — names ' + + 'against, `.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` — names ' + 'neither def and neither key: all thirteen exports of plugin-lifecycle-advanced.zod.ts and ' - + 'the string debounceDelay each occur 0 times across its 6409 tracked files, against lit ' - + 'controls objectstack 10171 and @objectstack/spec 3479 on the same corpus.', + + 'the string debounceDelay each occur 0 times across its 8228 tracked files, against lit ' + + 'controls objectstack 12966 and @objectstack/spec 4997 on the same corpus.', acceptanceCriteria: 'Every producer and reader of a PluginHealthCheck spells intervalMs and timeoutMs, and every ' + 'one of a HotReloadConfig spells debounceDelayMs — concretely ' diff --git a/packages/spec/src/migrations/entries/semantic/18.kernel-runtime-config-timeout-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.kernel-runtime-config-timeout-unit-in-key.ts index de3b9cbd8ef..7adc10fd67e 100644 --- a/packages/spec/src/migrations/entries/semantic/18.kernel-runtime-config-timeout-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.kernel-runtime-config-timeout-unit-in-key.ts @@ -42,9 +42,9 @@ export const entry: SemanticMigration = { + 'spells timeout 0 times; outside the zod file and its test the only live occurrences are the ' + 'generated rows in content/docs/references/kernel/plugin-security-advanced.mdx, which this ' + 'rename regenerates. The pinned objectui checkout — this is the pin we build against, ' - + '`.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694`, re-read from this tree — ' + + '`.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2`, re-read from this tree — ' + 'spells resourceLimits.timeout 0 times across ' - + '6409 tracked files, against lit controls timeout 832, RuntimeConfig 236 and resourceLimits ' + + '8228 tracked files, against lit controls timeout 1075, RuntimeConfig 240 and resourceLimits ' + '2 on the same corpus; both resourceLimits hits are prose in packages/app-shell recording ' + 'that objectui\'s own AppShellRuntimeConfig shares not one key with the spec\'s ' + 'RuntimeConfig, so nothing there authors this key and no pin bump is owed. #15939, #15678, ' diff --git a/packages/spec/src/migrations/entries/semantic/18.logging-durations-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.logging-durations-unit-in-key.ts index 9af3b362985..058151bb6d3 100644 --- a/packages/spec/src/migrations/entries/semantic/18.logging-durations-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.logging-durations-unit-in-key.ts @@ -45,9 +45,9 @@ export const entry: SemanticMigration = { + 'no in-repo runtime reads any of the four — outside `packages/spec/src/system/logging.zod.ts` ' + 'and its test the only occurrences are the generated rows in ' + '`content/docs/references/system/logging.mdx`, which this rename regenerates; and the pinned ' - + 'objectui checkout — `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` — spells ' + + 'objectui checkout — `.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` — spells ' + '`flushInterval` 0 times, `initialDelay` 0, `HttpDestinationConfig` 0 and `LoggingConfig` 0 ' - + 'across its 6409 tracked files, against lit controls `useState` 2304 and `timeout` 702 on ' + + 'across its 8228 tracked files, against lit controls `useState` 2383 and `timeout` 1075 on ' + 'the same corpus.', acceptanceCriteria: 'Every HTTP log destination spells `batch.flushIntervalMs`, `retry.initialDelayMs` and ' diff --git a/packages/spec/src/migrations/entries/semantic/18.system-metrics-jsdoc-durations-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.system-metrics-jsdoc-durations-unit-in-key.ts index b29ceee97e5..5afafd579ac 100644 --- a/packages/spec/src/migrations/entries/semantic/18.system-metrics-jsdoc-durations-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.system-metrics-jsdoc-durations-unit-in-key.ts @@ -67,10 +67,10 @@ export const entry: SemanticMigration = { + 'against a lit control of 1195 defineStack occurrences on that same corpus at fc28c1d38 ' + '(1195 again at 9b62f54671); and the objectui ' + 'checkout this repo builds against — this is the pin, ' - + '`.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694`, re-read from this tree — ' - + 'spells all six metrics def names and both distinctive keys 0 times across 6409 tracked ' - + 'files at that sha, against lit controls window 2710, timeout 832, period 160, ' - + 'interval 156 and metrics 301 on that same corpus and sha, so no pin bump is owed. ' + + '`.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2`, re-read from this tree — ' + + 'spells all six metrics def names and both distinctive keys 0 times across 8228 tracked ' + + 'files at that sha, against lit controls window 3464, timeout 1075, period 170, ' + + 'interval 170 and metrics 324 on that same corpus and sha, so no pin bump is owed. ' + '#15939, #15679, #14478, ADR-0087.', acceptanceCriteria: 'Every metric definition spells summary.maxAgeSeconds, every error-budget burn rate window ' diff --git a/packages/spec/src/migrations/entries/semantic/18.system-tracing-otel-exporter-durations-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.system-tracing-otel-exporter-durations-unit-in-key.ts index 4f65e369166..4dc94bd724b 100644 --- a/packages/spec/src/migrations/entries/semantic/18.system-tracing-otel-exporter-durations-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.system-tracing-otel-exporter-durations-unit-in-key.ts @@ -63,11 +63,11 @@ export const entry: SemanticMigration = { + 'dark control of 0; inside packages/spec the ' + 'only occurrences are tracing.zod.ts, its test, and the generated rows in ' + 'content/docs/references/system/tracing.mdx, which this rename regenerates. And the ' - + 'pinned objectui checkout — `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` — names none of it: all 37 exports of ' - + 'tracing.zod.ts and each of the four key names occur 0 times across the 6409 files ' - + 'tracked at that sha (the 404 Span and 40 SpanSchema hits are objectui\'s own HTML ' + + 'pinned objectui checkout — `.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` — names none of it: all 37 exports of ' + + 'tracing.zod.ts and each of the four key names occur 0 times across the 8228 files ' + + 'tracked at that sha (the 486 Span and 53 SpanSchema hits are objectui\'s own HTML ' + 'text-span component, TextSpanSchema, an unrelated name), against two lit controls on ' - + 'that same corpus and sha: 10171 hits for the bare token objectstack, and 3479 for the ' + + 'that same corpus and sha: 12966 hits for the bare token objectstack, and 4997 for the ' + 'package specifier @objectstack/spec.', acceptanceCriteria: 'Every author and reader of an OpenTelemetryCompatibility spells exporter.timeoutMs, ' diff --git a/packages/spec/src/migrations/entries/semantic/18.tenant-schema-cache-ttl-unit-in-key.ts b/packages/spec/src/migrations/entries/semantic/18.tenant-schema-cache-ttl-unit-in-key.ts index 725d8d7d806..29f3ebd5c33 100644 --- a/packages/spec/src/migrations/entries/semantic/18.tenant-schema-cache-ttl-unit-in-key.ts +++ b/packages/spec/src/migrations/entries/semantic/18.tenant-schema-cache-ttl-unit-in-key.ts @@ -28,8 +28,8 @@ export const entry: SemanticMigration = { + 'bd25e897dc: no in-repo runtime reads the key — outside `packages/spec/src/system/tenant.zod.ts` ' + 'and its test the only occurrences are the four generated rows in ' + '`content/docs/references/system/tenant.mdx`, which this rename regenerates; and the pinned ' - + 'objectui checkout — `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` — spells it 0 ' - + 'times across 6409 tracked files, against lit controls `TTL` 112 and `tenant` 819 on the ' + + 'objectui checkout — `.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` — spells it 0 ' + + 'times across 8228 tracked files, against lit controls `TTL` 156 and `tenant` 976 on the ' + 'same corpus.', acceptanceCriteria: 'Every schema-level tenant isolation source spells `performance.schemaCacheTtlSeconds`; ' diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index c336cc1e393..83047789d61 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -9311,10 +9311,10 @@ const step18: MigrationStep = { + 'the audience that does not parse. Measured on 884e8347d: the only in-repo readers are ' + 'packages/core/src/health-monitor.ts and packages/core/src/hot-reload.ts, both moved in ' + 'this same change; and the pinned objectui checkout — the pin this repo builds ' - + 'against, `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` — names ' + + 'against, `.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` — names ' + 'neither def and neither key: all thirteen exports of plugin-lifecycle-advanced.zod.ts and ' - + 'the string debounceDelay each occur 0 times across its 6409 tracked files, against lit ' - + 'controls objectstack 10171 and @objectstack/spec 3479 on the same corpus.', + + 'the string debounceDelay each occur 0 times across its 8228 tracked files, against lit ' + + 'controls objectstack 12966 and @objectstack/spec 4997 on the same corpus.', acceptanceCriteria: 'Every producer and reader of a PluginHealthCheck spells intervalMs and timeoutMs, and every ' + 'one of a HotReloadConfig spells debounceDelayMs — concretely ' @@ -9500,9 +9500,9 @@ const step18: MigrationStep = { + 'spells timeout 0 times; outside the zod file and its test the only live occurrences are the ' + 'generated rows in content/docs/references/kernel/plugin-security-advanced.mdx, which this ' + 'rename regenerates. The pinned objectui checkout — this is the pin we build against, ' - + '`.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694`, re-read from this tree — ' + + '`.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2`, re-read from this tree — ' + 'spells resourceLimits.timeout 0 times across ' - + '6409 tracked files, against lit controls timeout 832, RuntimeConfig 236 and resourceLimits ' + + '8228 tracked files, against lit controls timeout 1075, RuntimeConfig 240 and resourceLimits ' + '2 on the same corpus; both resourceLimits hits are prose in packages/app-shell recording ' + 'that objectui\'s own AppShellRuntimeConfig shares not one key with the spec\'s ' + 'RuntimeConfig, so nothing there authors this key and no pin bump is owed. #15939, #15678, ' @@ -9644,9 +9644,9 @@ const step18: MigrationStep = { + 'no in-repo runtime reads any of the four — outside `packages/spec/src/system/logging.zod.ts` ' + 'and its test the only occurrences are the generated rows in ' + '`content/docs/references/system/logging.mdx`, which this rename regenerates; and the pinned ' - + 'objectui checkout — `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` — spells ' + + 'objectui checkout — `.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` — spells ' + '`flushInterval` 0 times, `initialDelay` 0, `HttpDestinationConfig` 0 and `LoggingConfig` 0 ' - + 'across its 6409 tracked files, against lit controls `useState` 2304 and `timeout` 702 on ' + + 'across its 8228 tracked files, against lit controls `useState` 2383 and `timeout` 1075 on ' + 'the same corpus.', acceptanceCriteria: 'Every HTTP log destination spells `batch.flushIntervalMs`, `retry.initialDelayMs` and ' @@ -11836,10 +11836,10 @@ const step18: MigrationStep = { + 'against a lit control of 1195 defineStack occurrences on that same corpus at fc28c1d38 ' + '(1195 again at 9b62f54671); and the objectui ' + 'checkout this repo builds against — this is the pin, ' - + '`.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694`, re-read from this tree — ' - + 'spells all six metrics def names and both distinctive keys 0 times across 6409 tracked ' - + 'files at that sha, against lit controls window 2710, timeout 832, period 160, ' - + 'interval 156 and metrics 301 on that same corpus and sha, so no pin bump is owed. ' + + '`.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2`, re-read from this tree — ' + + 'spells all six metrics def names and both distinctive keys 0 times across 8228 tracked ' + + 'files at that sha, against lit controls window 3464, timeout 1075, period 170, ' + + 'interval 170 and metrics 324 on that same corpus and sha, so no pin bump is owed. ' + '#15939, #15679, #14478, ADR-0087.', acceptanceCriteria: 'Every metric definition spells summary.maxAgeSeconds, every error-budget burn rate window ' @@ -12031,11 +12031,11 @@ const step18: MigrationStep = { + 'dark control of 0; inside packages/spec the ' + 'only occurrences are tracing.zod.ts, its test, and the generated rows in ' + 'content/docs/references/system/tracing.mdx, which this rename regenerates. And the ' - + 'pinned objectui checkout — `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` — names none of it: all 37 exports of ' - + 'tracing.zod.ts and each of the four key names occur 0 times across the 6409 files ' - + 'tracked at that sha (the 404 Span and 40 SpanSchema hits are objectui\'s own HTML ' + + 'pinned objectui checkout — `.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` — names none of it: all 37 exports of ' + + 'tracing.zod.ts and each of the four key names occur 0 times across the 8228 files ' + + 'tracked at that sha (the 486 Span and 53 SpanSchema hits are objectui\'s own HTML ' + 'text-span component, TextSpanSchema, an unrelated name), against two lit controls on ' - + 'that same corpus and sha: 10171 hits for the bare token objectstack, and 3479 for the ' + + 'that same corpus and sha: 12966 hits for the bare token objectstack, and 4997 for the ' + 'package specifier @objectstack/spec.', acceptanceCriteria: 'Every author and reader of an OpenTelemetryCompatibility spells exporter.timeoutMs, ' @@ -12131,8 +12131,8 @@ const step18: MigrationStep = { + 'bd25e897dc: no in-repo runtime reads the key — outside `packages/spec/src/system/tenant.zod.ts` ' + 'and its test the only occurrences are the four generated rows in ' + '`content/docs/references/system/tenant.mdx`, which this rename regenerates; and the pinned ' - + 'objectui checkout — `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694` — spells it 0 ' - + 'times across 6409 tracked files, against lit controls `TTL` 112 and `tenant` 819 on the ' + + 'objectui checkout — `.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` — spells it 0 ' + + 'times across 8228 tracked files, against lit controls `TTL` 156 and `tenant` 976 on the ' + 'same corpus.', acceptanceCriteria: 'Every schema-level tenant isolation source spells `performance.schemaCacheTtlSeconds`; ' diff --git a/packages/spec/src/ui/component.test.ts b/packages/spec/src/ui/component.test.ts index 13cc425e654..a4e52411d59 100644 --- a/packages/spec/src/ui/component.test.ts +++ b/packages/spec/src/ui/component.test.ts @@ -279,18 +279,19 @@ describe('PageAccordionProps variant (#6776)', () => { // sweep once read as declared-but-unenforced. It has a live cross-repo consumer: // objectui's `PageAccordionRenderer` renders `{item.icon && }` inside the `AccordionTrigger` -// (`packages/components/src/renderers/layout/containers.tsx:919-925`), and the +// (`packages/components/src/renderers/layout/containers.tsx:1069-1075`), and the // same file's `ComponentRegistry.register('accordion', …)` publishes the key to -// the Studio block designer at `:966` (the `items` input, documented as +// the Studio block designer at `:1116` (the `items` input, documented as // `[{ label, icon?, collapsed?, children }]`). Measured at the pin this repo -// builds against — `.objectui-sha` = `53ded82bf`. Re-derived at that pin -// 2026-09-08: `containers.tsx` is byte-identical to the one at `a472b0716` -// (and, through it, to `00d3f09c5` — the last hop on which either anchor -// moved, both by exactly one line, `918-924` to `919-925` and `965` to `966`), -// so NO anchor moved here — the icon block still spans `919-925` and the -// registration input still lands on `:966`. Both were re-READ at the new pin -// rather than inferred from that identity, because identity preserves a wrong -// anchor as faithfully as a right one (#10274). +// builds against — `.objectui-sha` = `87af769e9`. Re-derived at that pin +// 2026-09-20: `containers.tsx` is NO LONGER byte-identical to the one at +// `53ded82bf` (300 insertions, 100 deletions), so both anchors were re-READ +// rather than carried — the icon block MOVED `919-925` to `1069-1075` with its +// seven lines byte-identical, and the registration input MOVED `966` to `1116` +// with its LINE rewritten (it now declares `of: 'object'` and carries a longer +// description, and no longer carries a `label`), while the member list this +// pin cites is unchanged. Identity preserves a wrong +// anchor as faithfully as a right one, which is why neither was carried (#10274). // // #9397 spent a full dispatch cycle re-deriving that read point from scratch // after the sweep proposed retiring the key. This block plus the `.describe()` @@ -369,17 +370,18 @@ describe('PageTabsProps items[].value / items[].count (#5775)', () => { // same bare declaration a liveness sweep reads as declared-but-unenforced. // objectui's `PageTabsRenderer` renders `{item.icon && }` inside the `TabsTrigger` -// (`packages/components/src/renderers/layout/containers.tsx:730-736`), and the +// (`packages/components/src/renderers/layout/containers.tsx:853-859`), and the // same file's `ComponentRegistry.register('tabs', …)` publishes the key to the -// Studio block designer at `:789` (the `items` input, documented as +// Studio block designer at `:912` (the `items` input, documented as // `[{ label, value?, icon?, count?, visibleWhen?, children }]`). Measured at -// the pin this repo builds against — `.objectui-sha` = `53ded82bf`. Re-derived -// at that pin 2026-09-08: `containers.tsx` is byte-identical to the one at -// `a472b0716` (and, through it, to `00d3f09c5` — the last hop on which either -// anchor moved, both by exactly one line, `729-735` to `730-736` and `788` to -// `789`), so NO anchor moved here — the icon block still spans `730-736` and -// the registration input still lands on `:789`. Both were re-READ at the new -// pin, never inferred (#10274). +// the pin this repo builds against — `.objectui-sha` = `87af769e9`. Re-derived +// at that pin 2026-09-20: `containers.tsx` is NO LONGER byte-identical to the +// one at `53ded82bf` (300 insertions, 100 deletions), so both anchors were +// re-READ rather than carried — the icon block MOVED `730-736` to `853-859` +// with its seven lines byte-identical, and the registration input MOVED `789` +// to `912` with its LINE rewritten (it now declares `of: 'object'` and carries +// a longer description, and no longer carries a `label`), while the member list +// this pin cites is unchanged. Never inferred (#10274). // // #9397 spent a full dispatch cycle re-deriving the accordion's read point // after the sweep proposed retiring it. This block plus the `.describe()` it @@ -3357,13 +3359,17 @@ describe('#7751 — object-* block props schemas', () => { // #16503 — the spec half of objectui#8172 (decision batch #68, 2026-09-07, // option A: the contract declares the capability that already ships, is // documented and is in use). Measured at the objectui pin this repo builds -// against (`.objectui-sha` = `53ded82bf`; all four anchors re-READ at that pin -// 2026-09-08 — every `plugin-kanban` file below is byte-identical to the one at -// `a472b0716`, and none moved): `plugin-kanban/src/ObjectKanban.tsx:264` -// queries `$top: schema.limit ?? DEFAULT_KANBAN_LIMIT` (100, `:71`), -// `plugin-kanban/src/index.tsx:395-398` maps `limit: 'limit'` in -// `OBJECT_KANBAN_DATA_SOURCE`, `plugin-kanban/src/types.ts:134` declares -// `KanbanSchema.limit?: number`, and `content/docs/plugins/plugin-kanban.mdx` +// against (`.objectui-sha` = `87af769e9`; all four anchors re-READ at that pin +// 2026-09-20 — this hop moved every one of them and renamed one face outright, +// so none is carried): `plugin-kanban/src/ObjectKanban.tsx:676` +// queries `$top: resolveRowLimit(schema.limit, DEFAULT_KANBAN_LIMIT)` (100, +// `:84`; the bare `??` became `resolveRowLimit` in objectui#9925, which drops +// and reports a cap the contract refuses), +// `plugin-kanban/src/index.tsx:447-450` maps `limit: 'limit'` in +// `OBJECT_KANBAN_DATA_SOURCE`, ⚠️ `KanbanSchema` is RETIRED at this pin and +// `plugin-kanban/src/types.ts` declares the member no more — the published +// twin is `ObjectKanbanSchema`, declaring `limit?: number` at +// `packages/types/src/objectql.ts:3735` — and `content/docs/plugins/plugin-kanban.mdx` // teaches `limit: 250` with a Properties row. The strict map refused the key by // name — the same `unrecognized_keys` verdict as the `bogusProp` control — so an // author following the published docs wrote a node the save gate rejected. @@ -3424,12 +3430,16 @@ describe('ObjectKanbanPropsSchema limit — the row cap four objectui faces alre // on the React-host `kanban-ui` block). Unlike `limit` above — a key four // objectui faces already implemented, so the spec was the half that was wrong // — `quickAdd` was FORWARDED and never read: at the pin this repo builds -// against (`.objectui-sha` = `53ded82bf`) `ObjectKanban.tsx:931` spreads the +// against (`.objectui-sha` = `87af769e9`; re-READ there 2026-09-20, every +// anchor MOVED with its cited text byte-identical) `ObjectKanban.tsx:1563` +// spreads the // authored bag into `KanbanRenderer` and `KanbanImpl` gates the affordance on -// `quickAdd && onQuickAdd` (`:355`, `:368`), while `onQuickAdd` is a +// `quickAdd && onQuickAdd` (`KanbanImpl.tsx:621`, `:634` — the file is spelled +// here because those two ranges are NOT in `ObjectKanban.tsx`), while +// `onQuickAdd` is a // host-supplied FUNCTION no producer puts on an `object-kanban` node -// (`ObjectKanban.tsx` names neither half: 0 each, against 6 for the sibling -// `onCardClick` in the same file). +// (`ObjectKanban.tsx` names neither half: 0 each re-counted at this pin, +// against 11 for the sibling `onCardClick` in the same file). describe('ObjectKanbanPropsSchema quickAdd is retired (#17260)', () => { const kanban = ComponentPropsMap['object-kanban']; @@ -3486,15 +3496,18 @@ describe('ObjectKanbanPropsSchema quickAdd is retired (#17260)', () => { // #9881 and #9972 recorded the accordion and tab items; these two close the set. // // The button record re-measured at the pin this repo builds against — -// `.objectui-sha` = `53ded82bf`, re-derived there 2026-09-08. Both files in -// this chain, `resolve-icon.ts` and `button.tsx`, are byte-identical to the -// ones at `a472b0716` and, through it, to `00d3f09c5`, so no anchor moved; -// every one below was still re-READ at the new pin rather than inferred from -// that identity (#10274). The hop +// `.objectui-sha` = `87af769e9`, re-derived there 2026-09-20. Both files in +// this chain moved on this hop — `resolve-icon.ts` +203/-7 and `button.tsx` +// +6/-11 against `53ded82bf` — so no anchor below is carried and every one +// was re-READ (#10274). ⚠️ `resolveIcon` itself was rewritten: its tail no +// longer indexes `lucide-react`'s `icons` record, it asks `recordIconName` +// for the kebab-case name and hands the pair to `lazyIconComponent`, so the +// glyph arrives lazily. What an author may write did not change with it. The +// earlier hop // onto `00d3f09c5` was the one that changed this record's SUBSTANCE and not // merely its line numbers: `resolve-icon.ts` was restructured (110 // insertions), so `resolveIcon` no longer PascalCases and maps inline — it -// delegates to the `describeIconLookup` seam (`:117-120`), and the tokeniser +// delegates to the `describeIconLookup` seam (now `:302-305`), and the tokeniser // splits on hyphen, underscore AND whitespace (`/[-_\s]+/`), where this record // used to say "splits on `-` only". That sentence was true when written and // was false by then, which is exactly why a citation refresh re-READS instead @@ -3516,11 +3529,13 @@ describe('ElementButtonPropsSchema icon liveness (#10053)', () => { it('accepts an icon on a button — the value objectui resolves through the lucide `icons` map', () => { // objectui `packages/components/src/renderers/form/button.tsx:43` hands the // name to the shared `resolveIcon` - // (`packages/components/src/renderers/action/resolve-icon.ts:129-132`), - // which delegates to `describeIconLookup` (`:117-120`): that PascalCases - // through `toPascalCase` (`:100-105`, splitting on hyphen, underscore or - // whitespace) and applies the one-entry rename map (`:90-92`) before the - // lookup in `icons` from `lucide-react`; `button.tsx:72` / `:74` + // (`packages/components/src/renderers/action/resolve-icon.ts:322-328`), + // which delegates to `describeIconLookup` (`:302-305`): that PascalCases + // through `toPascalCase` (`:153-158`, splitting on hyphen, underscore or + // whitespace) and applies the one-entry rename map (`:143-145`) before the + // lookup, which at this pin runs through `recordIconName` + + // `lazyIconComponent` rather than indexing `icons` from `lucide-react` + // directly; `button.tsx:72` / `:74` // draw it either side of the label per `iconPosition`. const result = button.safeParse({ label: 'Save', icon: 'arrow-right' }); expect(result.success).toBe(true); diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 4d341f733c8..31fb9f59513 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -692,18 +692,19 @@ export const PageTabsProps = strictObject({ * false candidate a component over). * * The key is LIVE at the objectui pin this repo builds against - * (`.objectui-sha` = `53ded82bf`; re-derived at that pin 2026-09-08 — - * `containers.tsx` is byte-identical to the one at `a472b0716` and, through - * it, to `00d3f09c5`, the last hop on which either anchor moved by exactly - * one line (the icon block from `729-735`, the registration input from - * `788`), so NO anchor moved here; - * both were re-READ at the new pin with the cited text unchanged rather - * than inferred from that identity): `containers.tsx:730-736` + * (`.objectui-sha` = `87af769e9`; re-derived at that pin 2026-09-20 — + * `containers.tsx` is NO LONGER byte-identical to the one at `53ded82bf` + * (300 insertions, 100 deletions), so both anchors were re-READ rather + * than carried: the icon block moved `730-736` -> `853-859` with its seven + * lines byte-identical, and the registration input moved `789` -> `912` + * with its LINE rewritten — it now declares `of: 'object'` and carries a + * longer description — while the member list this record cites is + * unchanged): `containers.tsx:853-859` * renders * `{item.icon && }` inside the * `TabsTrigger`, left of the label span (`mr-1.5 h-3.5 w-3.5 shrink-0 * opacity-70`, `aria-hidden`), and the renderer's registration publishes - * the key to the Studio block designer at `:789` (the `items` input, + * the key to the Studio block designer at `:912` (the `items` input, * documented as `[{ label, value?, icon?, count?, visibleWhen?, children * }]`). * @@ -1863,18 +1864,19 @@ export const PageAccordionProps = strictObject({ * re-derive the same false candidate). * * The key is LIVE at the objectui pin this repo builds against - * (`.objectui-sha` = `53ded82bf`; re-derived at that pin 2026-09-08 — - * `containers.tsx` is byte-identical to the one at `a472b0716` and, through - * it, to `00d3f09c5`, the last hop on which either anchor moved by exactly - * one line (the icon block from `918-924`, the registration input from - * `965`), so NO anchor moved here; - * both were re-READ at the new pin with the cited text unchanged rather - * than inferred from that identity): `containers.tsx:919-925` + * (`.objectui-sha` = `87af769e9`; re-derived at that pin 2026-09-20 — + * `containers.tsx` is NO LONGER byte-identical to the one at `53ded82bf` + * (300 insertions, 100 deletions), so both anchors were re-READ rather + * than carried: the icon block moved `919-925` -> `1069-1075` with its + * seven lines byte-identical, and the registration input moved `966` -> + * `1116` with its LINE rewritten — it now declares `of: 'object'` and + * carries a longer description — while the member list this record cites + * is unchanged): `containers.tsx:1069-1075` * renders * `{item.icon && }` inside the * `AccordionTrigger`, grouped with the label in the trigger's one wrapping * span, and the renderer's registration publishes the key to the Studio - * block designer at `:966` (the `items` input, documented as + * block designer at `:1116` (the `items` input, documented as * `[{ label, icon?, collapsed?, children }]`). * * Vocabulary is Lucide, resolved through objectui's `LazyIcon` @@ -2121,19 +2123,26 @@ export const ElementButtonPropsSchema = lazySchema(() => strictObject({ * the button. * * The key is LIVE at the objectui pin this repo builds against - * (`.objectui-sha` = `53ded82bf`; re-derived at that pin 2026-09-08 — both - * files, `resolve-icon.ts` and `button.tsx`, are byte-identical to the ones - * at `a472b0716` and, through them, to `00d3f09c5`, so no anchor MOVED; an - * earlier re-READ corrected two that - * had been wrong since they were written — the registration's input list and - * its `defaultProps`, see the last paragraph of this block. The hop onto - * `00d3f09c5` was the one that moved this record's SUBSTANCE rather than - * only its line numbers: `resolve-icon.ts` was restructured (110 insertions) - * so the resolution runs through a named `describeIconLookup` seam, and its - * tokeniser accepts more spellings than this record used to claim — see the - * corrected breakdown below. Every anchor below was re-READ at the new pin, - * never carried (#10274); the read point first MOVED rather than died on the - * earlier hop onto `9602dc820`, which is why the anchors span a second + * (`.objectui-sha` = `87af769e9`; re-derived at that pin 2026-09-20 — this + * hop is the one that moved both files: `resolve-icon.ts` +203/-7 and + * `button.tsx` +6/-11 against `53ded82bf`, so NO anchor below is carried and + * every one was re-READ. Four moved with their cited text byte-identical + * (`describeIconLookup`, `toPascalCase`, the rename map, `getLazyIcon`); + * `resolveIcon` itself was REWRITTEN — its tail no longer indexes + * `lucide-react` directly — and the registration's two ranges both moved and + * SHRANK, the input list having lost its per-input `label` and + * `defaultValue` members. What each anchor asserts is re-stated below from + * the new tree, never inferred from the old one — the re-READ discipline + * this block records, and which the later paragraph below still cites by + * number. ⚠️ That number does NOT resolve: objectstack issue 10274 was probed + * 2026-09-20 with `scripts/check-issue-citations.mjs --probe-cause` and came + * back minted, gone from the board and 404 on the web endpoint too — DELETED, + * not transferred. ⛔ No replacement number is guessed here: the live record + * of the discipline is THIS BLOCK, together with + * `check:objectui-pin-citations`, whose header states the same rule and whose + * refusal text enforces it. The read point + * first MOVED rather than died on the earlier hop onto `9602dc820`, which is + * why the anchors span a second * file): * `components/src/renderers/form/ * button.tsx:43` resolves `schema.icon` through the shared `resolveIcon`, @@ -2145,17 +2154,21 @@ export const ElementButtonPropsSchema = lazySchema(() => strictObject({ * this surface use — it is the `action:*` resolver, and the two accept * different spellings: * - here: `resolveIcon` - * (`components/src/renderers/action/resolve-icon.ts:129-132`) delegates - * to `describeIconLookup` (`:117-120`), which PascalCases through - * `toPascalCase` (`:100-105`) and then applies a one-entry rename map - * (`Home` becomes `House`, `:90-92`) before the `icons[key]` lookup from - * `lucide-react`. ⚠️ The tokeniser splits on hyphen, underscore OR + * (`components/src/renderers/action/resolve-icon.ts:322-328`) delegates + * to `describeIconLookup` (`:302-305`), which PascalCases through + * `toPascalCase` (`:153-158`) and then applies a one-entry rename map + * (`Home` becomes `House`, `:143-145`) before the lookup. ⚠️ That last + * step is what this hop rewrote: the resolver no longer indexes + * `lucide-react`'s `icons` record itself — it asks `recordIconName` for + * the kebab-case name and hands the pair to `lazyIconComponent`, so the + * glyph now arrives lazily. The accept/reject behaviour is unchanged. + * ⚠️ The tokeniser splits on hyphen, underscore OR * whitespace (`/[-_\s]+/`) as of this pin; this record previously said * "splits on `-` only", which was true when written and is not now — a * re-READ caught it, a line-number refresh would not have. An unknown * name still resolves to `null` and the button renders with NO icon and * no diagnostic anywhere. - * - `LazyIcon` / `getLazyIcon` (`components/src/lib/lazy-icon.tsx:66-92`): + * - `LazyIcon` / `getLazyIcon` (`components/src/lib/lazy-icon.tsx:98-124`): * normalises to kebab-case, checks the name against Lucide's own name * list, and degrades an unknown name to the `Database` glyph. * So a spelling that draws an icon in a tab trigger can draw nothing here. @@ -2170,11 +2183,15 @@ export const ElementButtonPropsSchema = lazySchema(() => strictObject({ * behaviour promised above is the same on both sides of that move. * * Also measured at the same pin: the renderer's registration publishes no - * `icon` input (`button.tsx:85-102` lists `label`, `variant`, `size`, - * `className`; `:103-107` is `defaultProps`), so the Studio block designer - * does not offer the key. Those two anchors read `70-87` / `88-92` until the + * `icon` input (`button.tsx:85-97` lists `label`, `variant`, `size`, + * `className`; `:98-102` is `defaultProps`), so the Studio block designer + * does not offer the key. Both ranges SHRANK on this hop — the inputs went + * `85-102` -> `85-97` and `defaultProps` `103-107` -> `98-102`, because each + * input dropped its `label` and `defaultValue` members; the four input names + * and the absence of an `icon` input are what was re-read, and both hold. + * They read `70-87` / `88-92` until the * `a472b0716` re-measure: wrong since written rather than shifted — - * `button.tsx` is byte-identical at `00d3f09c5` and `a472b0716`, so only a + * `button.tsx` was byte-identical at `00d3f09c5` and `a472b0716`, so only a * re-READ could find them and a line-number refresh never would (#10274). Unpublished is not unread — the header `icon` * above is refused for the second, not the first, and this docblock exists * to hold them apart. @@ -2899,21 +2916,22 @@ export const ObjectMetricPropsSchema = lazySchema(() => strictObject({ * same record for the metric tile. * * The key is LIVE at the objectui pin this repo builds against - * (`.objectui-sha` = `53ded82bf`; re-derived at that pin 2026-09-08 — all - * five files in the chain below, `index.tsx`, `ObjectMetricWidget.tsx`, - * `MetricWidget.tsx`, `MetricCard.tsx` and `lazy-icon.tsx`, are - * byte-identical to the ones at `a472b0716`, at `00d3f09c5` and at - * `67dadd602` before it, so - * NO anchor moved — the `object-metric` registration still begins at `:194` - * and the icon input still lands on `:204`. Every anchor below was re-READ - * at the new pin rather than inferred from that identity), and the chain + * (`.objectui-sha` = `87af769e9`; re-derived at that pin 2026-09-20 — EVERY + * file in the chain below moved on this hop, so no anchor is carried and + * each was re-READ: the `object-metric` registration now begins at `:227` + * and its icon input lands on `:237`. ⚠️ One thing the numbers do not + * carry: that input's `label: 'Icon (Lucide name)'` member is GONE — the + * whole registration dropped its per-input labels — so the key is still + * PUBLISHED to the designer, now as a bare + * `{ name: 'icon', type: 'string' }`. The four render-path anchors moved + * with their cited text byte-identical), and the chain * runs three files: - * `plugin-dashboard/src/index.tsx:204` publishes it as a designer input - * (`Icon (Lucide name)`) on the registered `object-metric` block; - * `ObjectMetricWidget.tsx:142` destructures it and forwards it at `:474` to - * `MetricWidget`; `MetricWidget.tsx:312-321` resolves it via + * `plugin-dashboard/src/index.tsx:237` publishes it as a designer input + * on the registered `object-metric` block; + * `ObjectMetricWidget.tsx:174` destructures it and forwards it at `:483` to + * `MetricWidget`; `MetricWidget.tsx:351-360` resolves it via * `getLazyIcon(icon)` — guarded on `typeof icon === 'string'`, because the - * React prop also accepts a ready-made node — and `:373-382` draws it in the + * React prop also accepts a ready-made node — and `:412-421` draws it in the * tinted square whose colour comes from `colorVariant`. * * ⚠️ Do not re-anchor this to `MetricCard.tsx`. That sibling calls @@ -2923,7 +2941,7 @@ export const ObjectMetricPropsSchema = lazySchema(() => strictObject({ * reaches. * * Vocabulary is Lucide via the `LazyIcon` module - * (`components/src/lib/lazy-icon.tsx:66-80`): kebab-case or PascalCase, + * (`components/src/lib/lazy-icon.tsx:98-112`): kebab-case or PascalCase, * normalised to kebab-case, degrading to the `Database` glyph when the name * is not a real Lucide icon — the same slot the container icons use, and the * opposite failure mode from `element:button`'s `icon`, which takes the older @@ -2992,15 +3010,22 @@ export type ObjectMetricPropsParsed = z.infer; * `$filter` handoff), `data` (:217-224), `cardTitle`/`titleField` (:233), * `cardFields` (:322), `swimlaneField`/`grouping` (:518-519), and via the * forwarded schema `coverImageField`/`conditionalFormatting` (`KanbanRenderer`, - * index.tsx — `ObjectKanban.tsx:931` spreads the authored bag into it). + * index.tsx — `ObjectKanban.tsx:1563` spreads the authored bag into it). * `quickAdd` sat on that forwarded list and is RETIRED (#17260, tombstoned * below): the sentence was true about the FORWARD and false about the READ, * which is how the key kept re-authorizing itself. `groupField` is the * DESIGNER's spelling with * zero read points (#7973 class) — aliased to the `groupBy` the board reads. * `limit` (#16503) was measured later, at the pin this repo builds against - * (`.objectui-sha` = `53ded82bf`; re-READ there 2026-09-08, file - * byte-identical to `a472b0716`, anchor unmoved): `ObjectKanban.tsx:264`, the `$top` of the + * (`.objectui-sha` = `87af769e9`; re-READ there 2026-09-20 — the file moved + * hard on this hop, +708/-79 against `53ded82bf`, and this anchor is one the + * numbers alone would have mis-carried: the read was a bare + * `$top: schema.limit ?? DEFAULT_KANBAN_LIMIT` at `:264` and is now + * `$top: resolveRowLimit(schema.limit, DEFAULT_KANBAN_LIMIT)`, objectui#9925 + * having put a refusal in front of it — a contract-refused row cap is dropped + * and reported at `:553` instead of being sent. The pinned fact is unchanged: + * `schema.limit` still lowers into the query's top-level `$top`): + * `ObjectKanban.tsx:676`, the `$top` of the * board's one query — its docblock below carries the four-face record. */ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ @@ -3041,18 +3066,25 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ * Row cap (#16503 — the spec half of objectui#8172; decision batch #68, * 2026-09-07, option A: the contract declares the capability that already * ships, is documented and is in use). Measured at the objectui pin this - * repo builds against (`.objectui-sha` = `53ded82bf`; all four anchors - * re-READ at that pin 2026-09-08, every `plugin-kanban` file byte-identical - * to the one at `a472b0716` and none moved), four faces agreed + * repo builds against (`.objectui-sha` = `87af769e9`; all four anchors + * re-READ at that pin 2026-09-20 — this hop moved every one of them, and one + * face was RENAMED rather than shifted, so nothing below is carried), four + * faces agree * while this map refused the key by name: the board's one query is - * `dataSource.find(objectName, { $filter: schema.filter, $top: schema.limit - * ?? DEFAULT_KANBAN_LIMIT })` (`plugin-kanban/src/ObjectKanban.tsx:262-266`, - * the default `100` at `:71` — a REAL top-level `$top` since objectui#4025; - * before that the cap sat under a `options` key no adapter read), + * `dataSource.find(objectName, { $filter: schema.filter, $top: + * resolveRowLimit(schema.limit, DEFAULT_KANBAN_LIMIT) })` + * (`plugin-kanban/src/ObjectKanban.tsx:674-679`, + * the default `100` at `:84` — a REAL top-level `$top` since objectui#4025; + * before that the cap sat under a `options` key no adapter read, and the + * bare `??` became `resolveRowLimit` in objectui#9925, which drops and + * reports a cap the contract refuses instead of sending it), * `OBJECT_KANBAN_DATA_SOURCE` maps `limit: 'limit'` - * (`plugin-kanban/src/index.tsx:395-398`), `KanbanSchema` — the type - * `ObjectKanban.tsx:143` reads `schema` through — declares `limit?: number` - * (`plugin-kanban/src/types.ts:134`), and `content/docs/plugins/plugin-kanban.mdx` + * (`plugin-kanban/src/index.tsx:447-450`), the type the board reads `schema` + * through is `ObjectKanbanSchema` — ⚠️ `KanbanSchema` was RETIRED on this hop + * (maintainer ruling 2026-09-09) and `plugin-kanban/src/types.ts` no longer + * declares the member at all — imported at `ObjectKanban.tsx:10` and + * declaring `limit?: number` at `packages/types/src/objectql.ts:3735`, + * and `content/docs/plugins/plugin-kanban.mdx` * teaches it with a typed snippet (`limit: 250`) plus a Properties row. So * an author following the published docs wrote a node the save gate * refused, with the same `unrecognized_keys` verdict a typo gets. @@ -3062,8 +3094,10 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ * table, not this key's. The component-level `dataSource.limit` overrides * this key, and a bound named view's `pagination.pageSize` is LOWERED INTO * it through the `limit: 'limit'` mapping only when the component authored - * none (`react/src/element-data-source/ElementDataSourceGate.tsx:236-241`, - * `readLimit`/`writeLimit` keyed by `ElementDataSourceLimitKey`). The board + * none (`react/src/element-data-source/ElementDataSourceGate.tsx:316-331`, + * `readLimit`/`writeLimit` keyed by `ElementDataSourceLimitKey`; the branch + * gained objectui#9899's presence-is-not-authorship test and a + * `describeDisplacedRowLimit` report on this hop). The board * has no `pagination` read point, so declaring that spelling here would name * a key the renderer ignores — the accepted-and-dropped defect this section * exists to remove. Same shape as the `element:record_picker` and @@ -3086,12 +3120,14 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ * React-host `kanban-ui` block). * * Measured at the objectui pin this repo builds against - * (`.objectui-sha` = `53ded82bf`): the board forwards the key — - * `ObjectKanban.tsx:931` spreads the authored bag into `KanbanRenderer`, + * (`.objectui-sha` = `87af769e9`; all four anchors re-READ at that pin + * 2026-09-20 — each MOVED with its cited text byte-identical): the board + * forwards the key — + * `ObjectKanban.tsx:1563` spreads the authored bag into `KanbanRenderer`, * which passes * `quickAdd={schema.quickAdd}` and `onQuickAdd={schema.onQuickAdd}` - * (`plugin-kanban/src/index.tsx:196`) — but the affordance is gated on - * BOTH (`KanbanImpl.tsx:355` and `:368`), and `onQuickAdd` is a + * (`plugin-kanban/src/index.tsx:313`) — but the affordance is gated on + * BOTH (`KanbanImpl.tsx:621` and `:634`), and `onQuickAdd` is a * host-supplied FUNCTION that JSON cannot carry and no producer puts on an * `object-kanban` node. `ObjectKanban.tsx` names neither half of the pair * (0 occurrences each, against 6 for the sibling `onCardClick` in the same diff --git a/packages/spec/src/ui/dataset.zod.ts b/packages/spec/src/ui/dataset.zod.ts index 5c90e77cd75..dc01c2ad81a 100644 --- a/packages/spec/src/ui/dataset.zod.ts +++ b/packages/spec/src/ui/dataset.zod.ts @@ -197,38 +197,47 @@ export const DatasetMeasureSchema = lazySchema(() => strictObject({ * * A DATE-valued measure (`min` / `max` over a date field) never reads a date * PATTERN here: `"YYYY-MM-DD"` is accepted by this schema, reaches the - * renderer, and produces the locale default. The shared date path takes a - * named STYLE instead, so a date-only value reads `format` as `short` - * (`Jul 4, '24`) or `relative` (`3 days ago` inside a ±7-day window, the - * absolute locale form outside it) — the same two words `DateCellRenderer` - * honours from `field.format` — while a DATETIME value ignores `format` - * altogether. + * renderer, and produces that arm's default face. The shared date path takes + * a named STYLE instead, and BOTH arms honour the same two words — `short` + * (`Jul 4, '24` for a date, `9/11/2026 9:30 am` for a datetime) and + * `relative` (`3 days ago` inside a ±7-day window, the absolute form outside + * it) — the same two words `DateCellRenderer` honours from `field.format`. + * + * ⚠️ The datetime half of that sentence is NEW at the pin below and is the + * one thing this record's previous revision got wrong the moment the pin + * moved: until objectui#8352 a DATETIME value ignored `format` altogether, + * and this docblock and the `describe` beneath it both said so. * * Measured at the pin this repo builds against (`.objectui-sha` = - * `53ded82bf`; re-derived at that pin 2026-09-08 — BOTH files in this chain - * changed over the hop off `a472b0716` (`dataset-format.ts` +23 lines, - * `date-display.ts` +108 under objectui#7443, "one home for the datetime - * display convention"), so every anchor below was re-READ there and three of - * the four MOVED; the substance is unchanged — the date-only arm still - * threads `format` as a STYLE and the datetime arm still takes none) in + * `87af769e9`; re-derived at that pin 2026-09-20 — every anchor below MOVED + * on this hop and one of them changed SUBSTANCE, so nothing here is carried: + * `formatMeasureDate`'s datetime arm no longer calls + * `formatDateTime(v, { locale })` unconditionally, it SELECTS a formatter, + * because `formatDateTime(value, options?)` has no style parameter to thread + * into and widening that published signature was refused) in * objectui * `packages/core/src/utils/dataset-format.ts`: `formatMeasure` routes a - * non-numeric value through `formatMeasureDate` (`:185-198`, was `:184-197`), + * non-numeric value through `formatMeasureDate` (`:229-264`, was `:185-198`) + * at `:370`, * whose date-only arm threads `format` into the STYLE parameter of - * `formatDate` (`utils/date-display.ts:131-164`, was `:104-137`, whose + * `formatDate` (`utils/date-display.ts:198-233`, was `:131-164`, whose * `relative` branch falls back to the absolute form beyond ±7 days at - * `:117`, was `:90`), while its datetime arm calls - * `formatDateTime(v, { locale })` with no style at all (`:195`, was `:194`). + * `:152`, was `:117` — the fallback now strips the style through + * `absoluteFallbackOptions`), while its datetime arm answers `relative` with + * `formatRelativeDate` (`:260`), `short` with + * `formatDateTime(v, { locale, style: 'compact' })` (`:261`) and everything + * else — a date PATTERN included — with the bare + * `formatDateTime(v, { locale })` (`:262`). * Teaching * the shared path a pattern grammar would change every list cell that reads - * it, so the gap is DOCUMENTED here rather than closed (objectui#7178 ruled - * A; the datetime half is objectui#7443). + * it, so that gap is still DOCUMENTED here rather than closed (objectui#7178 + * ruled A; the datetime half is objectui#7443 and objectui#8352). */ format: z.string().optional().describe( 'Numeral pattern for a NUMERIC measure — grouping, decimals, percent; e.g. "0,0.00", "0.0%". ' + 'An amount takes its symbol from `currency`, not from a "$" in the pattern. A DATE-valued ' - + 'measure never reads a date pattern: `"YYYY-MM-DD"` renders the locale default. A date-only ' - + 'value reads `format` as a display style (`short`, `relative`); a datetime value ignores it.', + + 'measure never reads a date pattern: `"YYYY-MM-DD"` renders that arm\'s default face. A date ' + + 'or datetime value reads `format` as a display style — `short` or `relative`, honoured on both.', ).meta({ title: 'Format' }), /** * Display currency (ISO 4217, e.g. "USD", "CNY"). Carried onto the result diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 5468c395880..32e4b6b5a8d 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1847,29 +1847,35 @@ export const TreeConfigSchema = lazySchema(() => strictObject({ * Closed (strict) from the start, and the strictness stands on its own: it does * NOT rest on the renderer refusing an undeclared key, because nothing * downstream refuses one. Measured at the `.objectui-sha` pin `53ded82b` by - * EXECUTING the pinned declarations, not by reading them — and each anchor - * below quotes the line it was read at, so the next pin bump reds instead of - * rotting (`check:objectui-pin-citations`): + * EXECUTING the pinned declarations, not by reading them, and RE-READ at pin + * `87af769e9` on 2026-09-20 — every one of the seven anchors below moved on + * that hop, one of them lost the symbol it quoted, and the quotes now read the + * NEW tree; each anchor quotes the line it was read at, so the next pin bump + * reds instead of rotting (`check:objectui-pin-citations`): * * - **The block this face feeds is FLATTENED, not forwarded.** `ListView` - * (`packages/plugin-list/src/ListView.tsx:113` first line + * (`packages/plugin-list/src/ListView.tsx:146` first line * `function resolveListMapConfig(schema: { map?: unknown; options?: { map?: unknown } }): Record {`) - * and `ObjectView` (`packages/plugin-view/src/ObjectView.tsx:1381` first line + * and `ObjectView` (`packages/plugin-view/src/ObjectView.tsx:1764` first line * `case 'map':`) copy it through a HAND-LISTED whitelist - * (`packages/plugin-list/src/ListView.tsx:67` first line - * `export const FLAT_MAP_CONFIG_KEYS = [`) — this block's keys minus - * `style` — and emit those as flat props. An undeclared key IS dropped + * (`packages/plugin-list/src/ListView.tsx:85` first line + * `export const FLAT_MAP_CONFIG_SPELLING = {`) — ⚠️ re-read at the new pin: + * the whitelist was a key LIST named `FLAT_MAP_CONFIG_KEYS` carrying this + * block's keys MINUS `style`, and objectui#9950 made it a total map from + * every declared key to its flat spelling, `style` delivered as `mapStyle`. + * So all eight keys now reach the product — and emit those as flat props. An + * undeclared key IS still dropped * there, but by a whitelist and in SILENCE: no parse, no warning, no * diagnostic of any kind. * - **The renderer's own zod schema does not close the set.** - * `packages/types/src/zod/objectql.zod.ts:562` first line + * `packages/types/src/zod/objectql.zod.ts:1574` first line * `export const ObjectMapConfigSchema = z.object({` — a plain `z.object`, * NOT strict, so an undeclared key parses clean there: zero issues, no * warning. `getMapConfig` consults that `safeParse` - * (`packages/plugin-map/src/ObjectMap.tsx:373` first line + * (`packages/plugin-map/src/ObjectMap.tsx:385` first line * `const result = ObjectMapConfigSchema.safeParse(config);`) only to decide * whether to `console.warn`, then returns a spread of the AUTHORED block - * (`:378` first line `return { ...config, style: config.style || style };`), + * (`:390` first line `return { ...config, style: config.style || style };`), * undeclared key and all. That spread is reached by objectui's own * component-node `map` prop, never by this face's flatten product ("neither * flattener emits a `map` key at all", `getMapConfig`). @@ -1878,7 +1884,7 @@ export const TreeConfigSchema = lazySchema(() => strictObject({ * checker at all: it dies in the whitelist without a word, and the one schema * that could have reported it is open and warn-only. And this parse is the only * place an author is told ANYWHERE: `map` is not in objectui's - * `LIST_VIEW_LOCAL_OVERRIDES` (`packages/types/src/zod/objectql.zod.ts:313` + * `LIST_VIEW_LOCAL_OVERRIDES` (`packages/types/src/zod/objectql.zod.ts:734` * first line `const LIST_VIEW_LOCAL_OVERRIDES = [`), so objectui's own * `ListViewSchema` imports THIS block by reference and the document check on * that side is this same schema. The two key sets MIRROR each other, key for @@ -1888,11 +1894,12 @@ export const TreeConfigSchema = lazySchema(() => strictObject({ * (`schema.map?.style`) while this block did not declare it, so strictness here * refused a style URL the renderer honours — an author could not declare a map * style through this face at all (#18406, director decision batch #153 item 4). - * The two key sets match again. Measured at the `.objectui-sha` pin `53ded82b`: - * `packages/types/src/zod/objectql.zod.ts:562` declares the eight keys, - * `packages/plugin-map/src/ObjectMap.tsx:365` reads + * The two key sets match again. Re-read at the `.objectui-sha` pin `87af769e9` + * (2026-09-20; each of these three moved on the hop and each was re-READ): + * `packages/types/src/zod/objectql.zod.ts:1574` declares the eight keys, + * `packages/plugin-map/src/ObjectMap.tsx:377` reads * `schema.mapStyle || schema.map?.style`, and objectui's own - * `content/docs/plugins/plugin-map.mdx:131` documents `style` in the block — + * `content/docs/plugins/plugin-map.mdx:143` documents `style` in the block — * so the divergence was against the documented surface this docblock cites, not * merely against the code. The gantt / tree blocks * above used to be this file's two `.passthrough()` exceptions (renderer-ahead @@ -2933,16 +2940,21 @@ const FormFieldBaseSchema = lazySchema(() => { colSpan: z.number().int().min(1).max(4).optional().describe("Absolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (`colSpan: 4` in a 3-column grid renders as 3); a `colSpan` within the column count renders as authored, and `colSpan: 1` emits no span class at all."), /** * [#2578] Relative field width. 'full' resolves to the form grid's full - * column count (`plugin-form` `resolveColSpan`); which container-query tiers - * receive the span class is the form renderer's, not this key's. - * At the `.objectui-sha` pin `53ded82bf7` the renderer emits the widest - * tier's class only, so at intermediate widths the field takes a single - * cell, not the row (objectstack#17328: one cell of two at 720px). objectui#9253 (objectui - * `bd09957380`, 2026-09-12, ahead of that pin) emits one clamped class per - * multi-column tier, making 'full' the whole row at every multi-column tier - * — re-read this block at the pin bump that absorbs it. + * column count (`plugin-form` `resolveColSpan`, `autoLayout.ts:153`); which + * container-query tiers receive the span class is the form renderer's, not + * this key's. + * At the `.objectui-sha` pin `87af769e9` the renderer emits one clamped + * col-span class per multi-column tier — `spanLadderFor` + * (`components/src/renderers/form/form.tsx:204-231`) walks the container + * class's tiers and emits a class each time a tier can give more cells than + * the field already holds — so 'full' is the whole row at every multi-column + * tier. ⚠️ This is the re-read the previous revision of this block asked for: + * objectui#9244 / objectui#9253 (objectui `bd09957380`, 2026-09-12) land + * inside the `53ded82bf7...87af769e9` range, so the widest-tier-only + * under-span this block used to record (#17328: one cell of two at + * 720px) no longer reproduces at the pin this repo builds against. */ - span: z.enum(['auto', 'full']).default('auto').describe("Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `53ded82bf7`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin only the widest tier's class is emitted (`@2xl:col-span-3` for a 3-column grid), so at intermediate widths the field takes a single cell, not the row (one of two at the 720px modal width; measured in Chromium at viewport widths 390, 720 and 1700)."), + span: z.enum(['auto', 'full']).default('auto').describe("Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `87af769e9`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest."), /** Custom widget override — only needed when auto-inference is insufficient */ widget: z.string().optional().describe('Custom widget/component name (overrides type-based inference)'), diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index 8389fcb570e..d861981910a 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -9,10 +9,10 @@ "a pin bump without regeneration goes RED there — that is the anti-rot half of the freshness gate.", "Regenerate + re-record: node scripts/gen-sdui-manifest-node.mjs (see its header for the offline mode)." ], - "objectuiSha": "53ded82bf7a494f54e344e19099dbf00854b8694", + "objectuiSha": "87af769e9a3ee28ace099fdd653d3ebd79fe82e2", "objectuiPackagesVersion": "17.6.0", "generator": "scripts/gen-sdui-manifest-node.mjs", - "generatedAt": "2026-09-08", + "generatedAt": "2026-09-20", "sha256": "49211fee7792cf51174930dc2c1be2169d5175f77a83491f68f90b3f0c19e69d", "components": 57 }