Skip to content

Release v0.2.1 with changeset automation - #77

Merged
huangyiirene merged 4 commits into
mainfrom
copilot/release-new-version-again
Jan 15, 2026
Merged

huangyiirene merged 4 commits into
mainfrom
copilot/release-new-version-again

Conversation

Copilot AI commented Jan 15, 2026 •

Copy link
Copy Markdown
Contributor

Version 0.2.1 Release - Complete ✅

Summary

Successfully prepared version 0.2.1 release for all Object UI packages using the changeset workflow.

Changes Completed

  • Fixed changeset configuration (removed non-existent @apps/* pattern)
  • Created changeset for patch release
  • Bumped all package versions from 0.2.0 to 0.2.1
  • Generated CHANGELOG.md files for all packages
  • Updated root CHANGELOG.md
  • Built all packages successfully
  • Verified all 95 tests pass
  • Code review completed
  • Added changeset file to pass CI check

Packages Released

All 11 main packages at v0.2.1:

  • @object-ui/types, core, react, components
  • @object-ui/designer, cli, data-objectql
  • @object-ui/plugin-charts, plugin-editor, plugin-markdown, plugin-kanban

VSCode extension at v0.1.1 (dependency update)

Release Notes

This patch release includes infrastructure improvements:

  • Fixed changeset configuration by removing invalid @apps/* pattern
  • Enhanced CI/CD workflows with GitHub Actions
  • Improved contributing documentation

Next Steps

Merge this PR to trigger automated npm publish via GitHub Actions.

Original prompt

发一个版本


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits January 15, 2026 08:20
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

Package Size Gzipped
components (index.js) 1719.04KB 345.21KB
core (index.js) 0.27KB 0.14KB
data-objectql (ObjectQLDataSource.js) 11.43KB 2.76KB
data-objectql (hooks.js) 5.43KB 1.32KB
data-objectql (index.js) 0.42KB 0.26KB
designer (index.js) 0.83KB 0.32KB
plugin-charts (AdvancedChartImpl-CGlEUqvZ.js) 47.44KB 11.16KB
plugin-charts (BarChart-C9og8_1e.js) 525.61KB 132.50KB
plugin-charts (ChartImpl-BO5n60y7.js) 4.87KB 1.95KB
plugin-charts (index-DR7MvR2L.js) 23.55KB 6.93KB
plugin-charts (index.js) 0.21KB 0.16KB
plugin-editor (MonacoImpl-DCiwKyYW.js) 18.97KB 5.77KB
plugin-editor (index-CpP31686.js) 21.90KB 6.66KB
plugin-editor (index.js) 0.19KB 0.15KB
plugin-kanban (KanbanImpl-CI3LQYrL.js) 76.88KB 20.43KB
plugin-kanban (index-DNMbL6y1.js) 23.20KB 6.83KB
plugin-kanban (index.js) 0.18KB 0.15KB
plugin-markdown (MarkdownImpl-DcFjhoD-.js) 256.34KB 64.33KB
plugin-markdown (index-D4WUb7Qy.js) 21.42KB 6.50KB
plugin-markdown (index.js) 0.19KB 0.15KB
react (SchemaRenderer.js) 1.00KB 0.51KB
react (index.js) 0.13KB 0.11KB
react (index.test.js) 0.14KB 0.14KB
types (api.js) 0.24KB 0.19KB
types (base.js) 0.24KB 0.19KB
types (complex.js) 0.17KB 0.16KB
types (crud.js) 0.24KB 0.20KB
types (data-display.js) 0.19KB 0.17KB
types (data.js) 0.23KB 0.18KB
types (disclosure.js) 0.18KB 0.17KB
types (feedback.js) 0.18KB 0.16KB
types (form.js) 0.17KB 0.16KB
types (index.js) 1.46KB 0.74KB
types (layout.js) 0.23KB 0.18KB
types (navigation.js) 0.17KB 0.16KB
types (overlay.js) 0.18KB 0.16KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@github-actions

Copy link
Copy Markdown
Contributor

✅ All checks passed!

  • ✅ Type check passed
  • ✅ Tests passed
  • ✅ Lint check completed

@huangyiirene

Copy link
Copy Markdown
Collaborator

@copilot fix 2026-01-15T08:27:39.3165536Z ##[group]Run # Check if there are any changesets
2026-01-15T08:27:39.3165943Z �[36;1m# Check if there are any changesets�[0m
2026-01-15T08:27:39.3166346Z �[36;1mif [ ! "$(ls -A .changeset/*.md 2>/dev/null | grep -v 'README.md')" ]; then�[0m
2026-01-15T08:27:39.3166746Z �[36;1m # Check if any package files were changed�[0m
2026-01-15T08:27:39.3167131Z �[36;1m if git diff --name-only origin/main...HEAD | grep -q '^packages/'; then�[0m
2026-01-15T08:27:39.3167635Z �[36;1m echo "❌ This PR modifies packages but doesn't include a changeset"�[0m
2026-01-15T08:27:39.3167994Z �[36;1m echo ""�[0m
2026-01-15T08:27:39.3168247Z �[36;1m echo "To add a changeset, run: pnpm changeset"�[0m
2026-01-15T08:27:39.3168528Z �[36;1m echo ""�[0m
2026-01-15T08:27:39.3168798Z �[36;1m echo "If this PR doesn't need a changeset (docs only, etc.),"�[0m
2026-01-15T08:27:39.3169177Z �[36;1m echo "add the 'skip-changeset' label to the PR"�[0m
2026-01-15T08:27:39.3169459Z �[36;1m echo ""�[0m
2026-01-15T08:27:39.3169994Z �[36;1m echo "📚 Learn more: https://github.com/objectstack-ai/objectui/blob/main/CONTRIBUTING.md#versioning-and-releases"�[0m
2026-01-15T08:27:39.3170557Z �[36;1m exit 1�[0m
2026-01-15T08:27:39.3170942Z �[36;1m else�[0m
2026-01-15T08:27:39.3171217Z �[36;1m echo "✅ No package changes detected, skipping changeset check"�[0m
2026-01-15T08:27:39.3171541Z �[36;1m fi�[0m
2026-01-15T08:27:39.3171698Z �[36;1melse�[0m
2026-01-15T08:27:39.3171875Z �[36;1m echo "✅ Changeset found"�[0m
2026-01-15T08:27:39.3172090Z �[36;1mfi�[0m
2026-01-15T08:27:39.3204753Z shell: /usr/bin/bash -e {0}
2026-01-15T08:27:39.3204992Z env:
2026-01-15T08:27:39.3205222Z PNPM_HOME: /home/runner/setup-pnpm/node_modules/.bin
2026-01-15T08:27:39.3205525Z ##[endgroup]
2026-01-15T08:27:39.3300854Z ❌ This PR modifies packages but doesn't include a changeset
2026-01-15T08:27:39.3301256Z
2026-01-15T08:27:39.3301442Z To add a changeset, run: pnpm changeset
2026-01-15T08:27:39.3301634Z
2026-01-15T08:27:39.3301892Z If this PR doesn't need a changeset (docs only, etc.),
2026-01-15T08:27:39.3302776Z add the 'skip-changeset' label to the PR
2026-01-15T08:27:39.3303111Z
2026-01-15T08:27:39.3303924Z 📚 Learn more: https://github.com/objectstack-ai/objectui/blob/main/CONTRIBUTING.md#versioning-and-releases
2026-01-15T08:27:39.3313420Z ##[error]Process completed with exit code 1.

Copilot AI changed the title [WIP] Publish new software version Release v0.2.1 with changeset automation Jan 15, 2026
Copilot AI requested a review from hotlong January 15, 2026 08:35
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>

Copilot AI commented Jan 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix 2026-01-15T08:27:39.3165536Z ##[group]Run # Check if there are any changesets
2026-01-15T08:27:39.3165943Z �[36;1m# Check if there are any changesets�[0m
2026-01-15T08:27:39.3166346Z �[36...

Fixed in commit 99cf828. Added a changeset file to satisfy the CI check. The changeset workflow expects PRs that modify packages to include a changeset file - it gets consumed when the PR is merged and the automated release workflow runs.

Copilot AI requested a review from huangyiirene January 15, 2026 08:42
@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

Package Size Gzipped
components (index.js) 1719.04KB 345.21KB
core (index.js) 0.27KB 0.14KB
data-objectql (ObjectQLDataSource.js) 11.43KB 2.76KB
data-objectql (hooks.js) 5.43KB 1.32KB
data-objectql (index.js) 0.42KB 0.26KB
designer (index.js) 0.83KB 0.32KB
plugin-charts (AdvancedChartImpl-CGlEUqvZ.js) 47.44KB 11.16KB
plugin-charts (BarChart-C9og8_1e.js) 525.61KB 132.50KB
plugin-charts (ChartImpl-BO5n60y7.js) 4.87KB 1.95KB
plugin-charts (index-DR7MvR2L.js) 23.55KB 6.93KB
plugin-charts (index.js) 0.21KB 0.16KB
plugin-editor (MonacoImpl-DCiwKyYW.js) 18.97KB 5.77KB
plugin-editor (index-CpP31686.js) 21.90KB 6.66KB
plugin-editor (index.js) 0.19KB 0.15KB
plugin-kanban (KanbanImpl-CI3LQYrL.js) 76.88KB 20.43KB
plugin-kanban (index-DNMbL6y1.js) 23.20KB 6.83KB
plugin-kanban (index.js) 0.18KB 0.15KB
plugin-markdown (MarkdownImpl-DcFjhoD-.js) 256.34KB 64.33KB
plugin-markdown (index-D4WUb7Qy.js) 21.42KB 6.50KB
plugin-markdown (index.js) 0.19KB 0.15KB
react (SchemaRenderer.js) 1.00KB 0.51KB
react (index.js) 0.13KB 0.11KB
react (index.test.js) 0.14KB 0.14KB
types (api.js) 0.24KB 0.19KB
types (base.js) 0.24KB 0.19KB
types (complex.js) 0.17KB 0.16KB
types (crud.js) 0.24KB 0.20KB
types (data-display.js) 0.19KB 0.17KB
types (data.js) 0.23KB 0.18KB
types (disclosure.js) 0.18KB 0.17KB
types (feedback.js) 0.18KB 0.16KB
types (form.js) 0.17KB 0.16KB
types (index.js) 1.46KB 0.74KB
types (layout.js) 0.23KB 0.18KB
types (navigation.js) 0.17KB 0.16KB
types (overlay.js) 0.18KB 0.16KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@github-actions

Copy link
Copy Markdown
Contributor

✅ All checks passed!

  • ✅ Type check passed
  • ✅ Tests passed
  • ✅ Lint check completed

@huangyiirene
huangyiirene marked this pull request as ready for review January 15, 2026 08:46
@huangyiirene
huangyiirene merged commit 099bcef into main Jan 15, 2026
11 checks passed
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 9, 2026
…he array (objectui#8221) (objectstack-ai#8758)

* feat(core)!: retire the legacy string `sort` clause — one spelling, the array

`convertSortToQueryParams` drops the string arm (objectui#8221, director
ruling, decision batch objectstack-ai#77, option B). Its signature narrows to
`QuerySortEntry[]`, and a string that still reaches it at runtime is REFUSED
with a `console.error` naming the array form, quoting what arrived and stating
that the query carries no `$orderby` — never a silent drop.

Every declaration that published a string arm narrows with it, TypeScript face
and zod mirror together: `ObjectGridSchema.sort`, `ObjectMapSchema.sort`,
`ObjectGanttSchema.sort`, plus the local `sort` inputs on `LineItemsPanel`,
`ObjectTimeline` and `deriveRelatedLists`'s ListView reader. Docs teach the
array only.

Deliberately untouched, measured rather than assumed: `record:related_list`'s
`'field'` / `'-field'` string is a DIFFERENT dialect, normalized by
`RelatedList.normalizeSortSpec`, and never reaches this sink.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w

* fix(examples,tooling): migrate the one authored string `sort` fixture and re-key the moved doc-example ledger row

`examples/schema-catalog/src/schemas/plugin-view/object-view-list.json` authored
`table.sort: "name asc"` — the retired clause. `pnpm check` reported it by name
("did not validate as an ObjectUI schema"), and `objectui validate` gave the
reason: `table -> sort` expected array, received string. Migrated to
`[{ "field": "name", "order": "asc" }]`; the warning count moved 4 -> 3, which
is the firing control that the fixed file is the one that moved.

`scripts/check-doc-example-types.mjs`'s ledger is keyed by FILE:LINE, and the
`objectql.ts` edits shifted `ObjectFormSchema`'s example block 1604 -> 1607.
Re-keyed; `scripts/__tests__/check-doc-example-types.test.ts` is green again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 17, 2026
…objectui#8254) (objectstack-ai#9605)

Fixes objectstack-ai#8254

The renderer half objectui#7928's option A requires before
`ObjectViewSchema.listViews` is mirrored by reference onto the
protocol's `ObjectListViewSchema` — decision batch objectstack-ai#70, 2026-09-07. The
mirror is never shipped as a type-only edit, so the renderer honours the
protocol's shape here and the declaration follows on that card.

## What the measurement found

The protocol declares a named view's `columns` as the union `string[] |
ListColumn[]`. The local `NamedListView` declares only `string[]`. Two
of the three routes out of `ObjectView` end in a **name slot** whose
consumers index each record by every entry — `ObjectGridSchema.fields`,
and the `fields` of the node `generateViewSchema` emits — and the
named-view segment reached both of them **raw**.

Measured on the base tree by rendering a fixture built with
`ObjectListViewSchema.parse(...)`: a named view carrying the
`ListColumn[]` half arrived at both name slots as the column objects
themselves, i.e. a non-empty field list naming nothing. That is verbatim
the failure the header of `tableColumnFieldNames` already describes for
`table.columns` — `ObjectKanban` skips its `highlightFields` fallback
whenever the list is non-empty, so it renders worse than an empty one —
and objectui#5269 already answered it at that boundary.

`viewColumnFieldNames` answers it at this one: the presence-preserving
twin of that fold, over `columnIdentity`, this repo's single converged
reader for "which field does this column entry name". It accepts no
spelling `columnIdentity` did not already accept, so it narrows a
declared union rather than widening a tolerated set. The **union** slots
on the same branches — `ObjectGridSchema.columns` and the delegated
`list-view` `columns` — keep taking the authored value raw, because
their declared shape holds it and narrowing there would drop a column's
own `label`. One value, two slots, each given the shape it declares.

Precedence does not move. An authored empty `columns` still stops the
`||` chain where it stopped before — deliberately unlike the
`table.columns` fold, whose fall-through to the deprecated
`table.fields` alias is a precedence decision belonging to that chain
alone. The only visible behaviour change is that column entries
resolving to no identity now collapse to an empty list instead of
reaching a name slot as nameless entries.

`filter` (spec rule objects, not the local tuple dialect), `sort`,
`label` and `data` needed **no** renderer change. They are pinned
anyway: "already works" is the claim that rots without an instrument.

Also gone: the stale `as any` on the named-view config's `label` read.
It was not load-bearing — `label` is a declared member — and under the
mirror `label` becomes optional, which the cast was hiding from the type
checker at the one read site that has to survive its absence. Declared
here rather than folded in silently.

## The pin


`packages/plugin-view/src/__tests__/ObjectView.specShapedNamedView-8254.test.tsx`
— 13 cases.

The spec leg of every fixture is built by running
`ObjectListViewSchema.parse(...)`, not by looking spec-shaped. A fixture
that merely resembles the protocol pins the author's memory of it; one
that has been through the protocol's own parser cannot drift from it,
and the schema is strict upstream, so the parse also proves the fixture
carries no local-dialect key.

Both legs ride **one** fixture, side by side in the same `expect`, which
is what the card asked for: asserting each leg against a hard-coded
literal in two separate cases would let both drift together and still
pass.

## Reverse verification — and where the prediction was wrong

Reverting the two `viewColumnFieldNames(...)` call sites to their raw
forwards, on the committed fix:

```
anchor occurrences BEFORE mutation: 2  (expect 2)
python replaced 2 occurrence(s)
anchor occurrences AFTER mutation: 0 (expect 0); raw-forward occurrences: 2 (expect 2)
HEAD blob:    db4c76a
mutated blob: e90ba0ad823b3fb2d5590b3c1be0d71601d1525e
  Test Files  1 failed (1)
       Tests  3 failed | 10 passed (13)
RESTORED: byte-identical to HEAD (db4c76a)
```

Predicted: the four `columns` cases red. **Measured: three, and the
prediction was wrong about which three.** Two of those four assert a
union slot the fix never touched, so they stayed green exactly as they
should — they are controls, not fix cases, and the prediction had
mis-sorted them; the third red came from the controls block instead, the
no-identity case, which rides the fold. The miss is recorded in the test
file's own header rather than re-fitted to the result.

The mutation reached the code under test directly: the test imports
`../ObjectView` by relative path, so no `dist/` sits between the edit
and the run. Both legs proved the edit on disk by occurrence count and
by `git hash-object` against the `HEAD` blob, and the restore is proved
by the blob hash plus an empty `git diff HEAD`, never by an exit code.

## Gates

Run at `de2276e`, exit codes captured on their own line after
redirection — never after a pipe.

| gate | exit |
| --- | --- |
| `vitest run packages/plugin-view/
packages/types/…/object-view-unmirrored-keys-7779.test.ts
packages/app-shell/…/ObjectView.relayRungCensus-7559.test.ts` | 0 — 42
files, 546 tests |
| `pnpm --filter @object-ui/plugin-view type-check` | 0 |
| `pnpm exec eslint .` in `packages/plugin-view` | 0 — 0 errors, 362
warnings, no ratchet on warnings by design |
| `check-changeset-presence` · `-fixed` · `-no-major` · `-claims` | 0 ·
0 · 0 · 0 |
| `check-control-bytes` · `check-new-cross-file-line-citations` | 0 · 0
|
| `check-test-path-roots` · `check-vi-mock-specifiers` · `-inherit` ·
`-override-shape` | 0 · 0 · 0 · 0 |
| `check-unreferenced-sources` · `check-spec-symbol-derivation` ·
`check-phantom-dependencies` | 0 · 0 · 0 |
| `check-governed-queue-guard --test` (the three changed paths) | 0 —
NOT GOVERNED, 3 paths against 5 surfaces, none matched |

`scripts/pm/dispatch-gates.mjs` lives in the sibling `objectstack`
checkout and **refuses** for objectui paths (exit 2, "a sister repo's
list is hand-derived from its own package manifest and its own workflow
files"), so this list was hand-derived from this repo's own
`package.json` scripts and `.github/workflows/`.

**Declared narrowing.** The manifest carries ninety-odd `check:*`
scripts. This run is the subset whose **input** this diff can move, plus
the two pins that re-derive from the edited file by AST —
`object-view-unmirrored-keys-7779.test.ts` and app-shell's
`ObjectView.relayRungCensus-7559.test.ts`, both of which parse
`packages/plugin-view/src/ObjectView.tsx`. The rest is the whole farm
and belongs to CI. `check-changeset-claims` reported five pending
changesets naming the edited file; all five were read and none is
falsified by this diff — they describe the non-grid fetch, the gantt
branch of `generateViewSchema`, the `title` composition and the
`viewTabBar` census, none of which this change touches.

Type-check needed the dependency closure built first (`turbo run build
--filter '@object-ui/plugin-view^...'`, exit 0); without it `tsc`
reports missing workspace `.d.ts` files, which is not a finding about
this diff.

## Acceptance notes

**The card's `data` sub-decision is already answered, and its stated
premise is dead.** The card describes an undeclared `data` reached
through a cast on the named-view config. On today's tree: `data` **is**
declared on `NamedListView` — by reference to the spec type, not a hand
copy — the cast is **gone**, and the protocol's own
`ObjectListViewSchema` declares `data` too. objectui#8980 landed all of
it under the director-seat ruling of 2026-09-13. The default direction
on this card ("remove the read") would now delete a read of a member
both faces declare, which the file's own comment forbids by name, so the
read stands unchanged and this PR decides nothing about it. Authored
population carrying an inline `data` block on a named view: **zero**,
measured structurally over every authored named view in `apps/`,
`examples/` and `content/`, with a live grep channel as the control.

**The card's member figures are stale.** "47 declared members, 6 read"
was the pre-objectui#8980 reading. The instrument that re-derives both
numbers is `object-view-unmirrored-keys-7779.test.ts`; read the counts
there rather than from this paragraph. Of the members the renderer reads
off a named view, exactly one has no counterpart in
`ObjectListViewSchema`: `options`, the untyped bag. Every other one
maps.

**The card's line anchor does not point where it says.** Re-derived
anchors are reported on the card.

**The landing package is `packages/plugin-view/src/ObjectView.tsx`**,
re-derived: `packages/app-shell/src/views/ObjectView.tsx` contains zero
occurrences of the named-view binding this card is about.

**Population count, for the conversion note the mirror will need.**
Twelve authored named views across `apps/` + `examples/` + `content/`;
`apps/` contributes none. Five are accepted by `ObjectListViewSchema` as
written, seven are refused — six for the now-required `columns`, two for
the tuple filter dialect. The refusals concentrate in the documentation
examples, so objectui#7928's mirror needs a docs pass shipped with it.
Counts taken on this card, on this tree; re-run the census before acting
on them.

**Not filed, noted only:** the `views`-prop segment beside the
named-view one at both name slots still forwards raw. It declares
`string[]` and is the objectui#5097 host-composition surface, so its
contract has not moved and there is nothing to honour yet; when a host
writes column objects there, it is that surface's card. The named-view
`sort` is a second, narrower asymmetry: `ObjectListViewSchema` still
admits a bare string sort clause while objectui retired that spelling
under decision batch objectstack-ai#77 and refuses it in the grid path. Both are
recorded for whoever lands the mirror, not carried here.

objectui#7928 is `pm:blocked` on this card and is **not** closed by this
pull request; it remains open, and its own half — the by-reference
declaration — is still to come.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017bzjQQsBrHdwBy17iHVVe9)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…ropping it in silence (objectstack-ai#9984)

Fixes objectstack-ai#9955

## What was wrong

`packages/core/src/utils/sort-query.ts` refused the retired **scalar**
`sort` clause out
loud, and dropped the **same clause one container deeper** in complete
silence. The
author's row order disappeared and the console said nothing.

Re-measured at source on this branch's base, objectui `origin/main`
`8a7e09fa1` — the
array arm had exactly two statements per entry, `continue` and
`out.push`, and no sink
call anywhere between the `Array.isArray` guard and the return.

### Three arms, before and after

Each row is a real run of `convertSortToQueryParams` from
`packages/core/src/utils/__tests__/sort-query.test.ts` on this branch.

| input | `$orderby` BEFORE | diagnostics BEFORE | `$orderby` AFTER |
diagnostics AFTER |
|---|---|---|---|---|
| CONTROL A — scalar `'name asc'` | `undefined` | 1 (scalar message) |
`undefined` | 1 (scalar message, byte-identical) |
| SUBJECT — `['name asc']` | `undefined` | **0** | `undefined` | **1
(entry message)** |
| CONTROL B — `[{ field: 'name', order: 'desc' }]` | `{ name: 'desc' }`
| 0 | `{ name: 'desc' }` | 0 |
| CONTROL C — `[{ order: 'asc' }]` | `undefined` | 0 | `undefined` | **0
— unchanged, on purpose** |
| MIXED — `[{ field: 'a', order: 'asc' }, 'b desc']` | `{ a: 'asc' }` |
0 | `{ a: 'asc' }` | 1 (names `'b desc'`) |

Only the SUBJECT row moves, and it moves only in the diagnostics column.

⭐ CONTROL C is the load-bearing one. `[{ order: 'asc' }]` is also an
entry that
contributes nothing, and its silence is **declared** in
`normalizeSortEntries`' docblock:
"an entry with no usable `field` is SKIPPED (it names nothing to order
by)". It stays
silent. That is what shows the seam was cut at `typeof entry ===
'string'` and not at
"this entry has no field", which would have converted a designed silence
into noise on
every partially-authored view.

## ⛔ The accepted input set is UNCHANGED — by one key and by one shape

`sort: ['name asc']` still contributes **no ordering**. Nothing that was
refused is now
accepted, and nothing that was accepted is now refused. Making the
array-of-strings
*work* would re-admit the retired spelling one container deeper and
reverse the director
ruling quoted in this file's own docblock — decision batch objectstack-ai#77,
2026-09-07, objectui#8221,
maintainer approved, option B: "The platform has one `sort` spelling,
the array,
everywhere."

Loud is not accepted. Pinned, not merely asserted, by
`ACCEPTED INPUT SET UNCHANGED — the string entry contributes exactly
nothing`, which
compares five arrays against their own string-free twins on both
`convertSortToQueryParams` and `normalizeSortEntries` — and which is the
one new case
that stays **green under the ablation below**, because it pins the
non-change rather than
the change.

The two docblock sentences that look contradictory are both still true,
and neither was
edited:

- "Not a lenient alias: an unusable input (a number, an object, an array
of strings)
yields `undefined` rather than a guess" — governs the RETURN VALUE.
Unchanged.
- "A retired string is REFUSED OUT LOUD, not dropped in silence" —
governs the
DIAGNOSTIC. This is the one that was being violated, and the only one
this branch moves.

## The dedupe key — the decision, and why the line count stays bounded

**A sibling reporter, and ARM-TAGGED keys in the one existing Set.**

`retiredKey(ARM, SPELLING)` returns the arm token, a newline, then the
spelling, so the
two key spaces are disjoint **by construction**: the arm token is one of
two fixed words
and neither contains a newline, so the text before the first newline
names the arm no
matter what an author wrote. A bare string prefix would only have been
disjoint until
somebody authored the prefix — the spellings come out of authored JSON.

Why not reuse `reportRetiredSortSpelling` with the entry as its key:

1. **The sentence an author acts on differs.** The scalar refusal can
say "this query
carries no `$orderby`" because refusing the whole value refuses the
whole ordering. One
bad entry among good ones does not: `[{ field: 'a', order: 'asc' }, 'b
desc']` still
orders by `a`. Reusing the scalar text would tell an author their rows
are unordered
while they are ordered by everything except the key they are hunting for
— worse than
   silence.
2. **On one shared raw key the first arm to fire silences the other**,
so a page carrying
both spellings would print a sentence about the wrong container. Pinned
by
`CONTROL A — the scalar arm is untouched, and the two messages differ`.

**The bound**: ONE line per distinct spelling per arm, however many
blocks inherit the
same bad view. That is the property the existing dedupe was built for —
this sink runs
inside the query memo of every object-bound block, so a related-list
derivation over an
object with N lists re-enters it N times per pass. Pinned by `BOUNDED —
N blocks
inheriting one bad view print ONE line, per spelling`, which drives 25
renders of one bad
view and asserts one line, then a second spelling and asserts two, then
clears the seam
and asserts three.

## The message text

`console.error`, unconditional, not dev-gated — the same channel and the
same severity
class as the scalar sibling, for the reason its docblock already argues:
this REFUSES an
authored row order, so the page renders in an order the author did not
ask for. Not the
`warnOnUnknownActionKeys` class, which reports keys nothing was ever
going to read.
Asserted against `console.warn` in the same body.

Both halves objectui#9031 ruled load-bearing survive in this arm too,
and they are judged
rather than eyeballed: the new case
`objectui#9031 — the ENTRY message prescribes metadata the installed
spec ACCEPTS` parses
the entries back out of the **real emitted string** and feeds them to
`SortItemSchema`
from the INSTALLED `@objectstack/spec`, exactly as the scalar pin does.
The message names
`ARRAY_FORM_EXAMPLE`, states `order` is required at publish, and states
that the ascending
default is a runtime tolerance and not permission to omit the key.

## One decision worth a reviewer's eye: the empty-string entry

`['']` stays **silent**, and that is symmetry with the scalar arm rather
than an exception
to the seam. `sort: ''` never reaches the loud scalar arm either — the
`!sort` guard
answers first, and the pre-existing case
`stays silent for values that were never the retired spelling` says why
in words: an empty
spelling is "the author asked for nothing", and reporting it would train
readers to ignore
the message. Inverting that one container deeper would recreate exactly
the inconsistency
this card is about, in the other direction.

Whitespace is NOT in the carve-out: `' '` is truthy, is loud on the
scalar arm today,
and is loud as an entry too. Both legs are the control in
`the '' entry keeps the scalar arm's silence — symmetry, not an
exception`.

## Reverse verification (ablation) — measured, on disk, from the
committed state

Declared direction before running: turns RED. Run through
`scripts/ablation-replace.mjs`, which proves the mutation reached the
disk and proves the
restore:

```
anchor   x1 -> x0
replace  x0 -> x1
blob     ea88c7b -> d046030a78eb15e235d3689f71bd65149bcac347
ok mutation landed
  -> Tests  7 failed | 32 passed (39)
ok restored: blob == HEAD (ea88c7b) and `git diff HEAD` is empty
```

The 7 that fall are exactly the new objectui#9955 cases that assert the
diagnostic. The 32
that hold include every pre-existing scalar refusal — so the sink is
demonstrably not dead,
and the new assertions are not vacuous.

## The plugin-grid pin this change invalidates — updated in the same
branch

<sub>This section was rewritten by the `domain:ui#2` seat at
2026-09-19T02:04Z, under the standing clause that a dev writes a PR body
once and the seat writes later corrections. It replaces a section headed
「⚠️ ONE RED」 that described a state which lasted one round; the evidence
below is the seat's own and the implementer's, each attributed in
place.</sub>

`packages/plugin-grid/src/__tests__/gridArrayArmOrderby-8973.test.tsx`
had one case pinning
the very silence this card removes, and that pin **anticipated this card
by name**. Its own
comment read:

> ⚠️ Deliberately NOT a new refusal diagnostic. objectui#8767 landed
route C for the
> retired STRING clause; widening that refusal to cover the
array-of-strings is that
> card's business, not this one's. This entry is dropped exactly as the
shared sink drops
> an unusable entry — silently.

objectui#9955 is that card, and it landed the widening. The implementer
stopped at the face
boundary and asked rather than riding; the seat granted route **A**
(verdict
`5738396331`) and expanded the declared face by that one file, on three
readings taken in
that act:

- open PR objectui#9924 modifies the same file — the module docblock,
the imports,
`afterEach`, and an appended section — and ⛔ does **not** touch this
case's hunk;
- every `errorSpy` assertion on that branch is one of the three
pre-existing ones, and its
appended section carries **no** `errorSpy` assertion and no
array-of-strings case at all;
- ⇒ the two edits are disjoint **in both directions**, so neither order
breaks the other and
the serialization the in-flight rule buys is buying nothing here — while
the wait it costs
  is unbounded.

### What the update asserts

`expectNoOrderBy(params)` stays **first and unchanged** — the RETURN
value moved nothing,
and that is the half a later reader is likeliest to get wrong. The
diagnostic half becomes
`toHaveBeenCalledTimes(1)` plus three assertions that **discriminate**
the entry refusal
from the scalar one: the message must carry the entry sentence's own
opening, must **not**
carry the scalar sentence's, and must quote the value that arrived. A
bare
`toHaveBeenCalled()`, or a match on `objectui#8221`, would be satisfied
just as well by the
scalar message — and the input in this case is an **array**.

### The fence held

`git diff -U0` on that file yields exactly one hunk header, `@@ -188,5
+188,22 @@`. The
scalar-refusal case, the imports, the `afterEach` and the module
docblock — PR objectui#9924's
hunks — are all outside it. `packages/core/src/utils/sort-query.ts`, its
test and the
changeset are byte-identical to the first commit.

### The reading

Same command, same denominators, both sides: the composite affected set
went from
`Test Files 1 failed | 165 passed (166)` / `Tests 1 failed | 3400 passed
(3401)` to
`Test Files 166 passed (166)` / `Tests 3401 passed (3401)`. ⭐ The
equality of the totals is
the reading — it says the one failure is gone **and nothing else
moved**. The whole
`plugin-grid` suite is green on its own: 143 files, 1348 tests.

An advisory is posted on PR objectui#9924 (`5738398717`) so its author
is not surprised; it
asks nothing of that seat.

## Verification

Run at commit `09d61f337`, from the repo root, per the repo's canonical
invocation.

- `pnpm exec vitest run packages/core/` — **158 files, 3319 tests, all
passed**.
- `pnpm --filter @object-ui/core run type-check` — **exit 0** (both
legs: `tsc --noEmit`
and `tsc -p tsconfig.test.json`), after `pnpm --filter
'@object-ui/core^...' build`,
  without which every reading is a stale-`dist` artefact (`TS6305`).
- Sink-aware consumers, run by name because the module's behaviour
changed even though its
  exported symbols did not — `deriveRelatedLists.inheritSort`,
  `RecordDetailView.relatedListInheritedSort-5795`, `serverSorting`,
`ObjectView.sortSink`, `ObjectView.namedViewSortArity`,
`gridRetiredStringSort-8767`,
  `ObjectCalendar.sortMembersReachTheWire-8171`,
  `ObjectKanban.structuredMembersReachTheirSinks-8313`,
  `kanban-calendar-filter-sort-8174`, `ListView` — **all passed**.
- `pnpm exec eslint . --no-inline-config --format json` — the **whole
repo**, not a
narrowing: 5151 files linted, 95 errors and 13387 warnings, **every one
of them
pre-existing**; both files this branch touches report 0 errors and 0
warnings. The
  population and the file count are read from that JSON, not estimated.
- `pnpm check:control-bytes` — OK, 8050 tracked text files scanned.
- `pnpm check:new-line-citations` — 0 new citations, exit 0.
- `node scripts/check-changeset-presence.mjs` — 2 published source files
changed,
  1 changeset declared, exit 0.

The one red is the out-of-face pin described above, and only that.

## Scope

⛔ Three things this branch deliberately does not touch, each declared
out of scope by the
dispatch order:

1. The `field`-less OBJECT entry — its silence is declared in the
docblock and is pinned
   as CONTROL C to keep it that way.
2. The empty array — objectui#9607, already closed as a measured no-op.
Not folded in.
3. The `object-grid` wire shape — route B on objectui#8767, declined by
the maintainer by
   name on 2026-09-10. Nowhere near this branch.

## Acceptance notes

- `noted, not filed` — the dedupe Set is now keyed by `ARM + newline +
SPELLING` while the
sibling sink `reportRetiredFieldType` (same module family, same "retired
spelling" idea)
still keys on a raw value. Not a defect and not filed: that sink has one
arm, so it has
nothing to collide with. Successor that would meet it: whoever gives
that sink a second
arm. Named here so the next reader of the two does not read the
asymmetry as an
  oversight.

This PR is a **DRAFT** and stays one: landing is the claiming seat's act
after review.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq

---
_Generated by [Claude
Code](https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq)_

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…only (objectui#10581) (objectstack-ai#10621)

Fixes objectstack-ai#10581

Clause-②: no

`Clause-②: no` — reference docs stop teaching a refused spelling; no
code, export or accept set moves.

## What changed

One row of `content/docs/api/schema-reference.md`, in the
`ObjectGridSchema` property table:

- before: `sort` | `string \| SortConfig[]` | "Default sort
configuration."
- after: `sort` | `SortConfig[]` | "Default sort configuration. The
string clause (`"name desc"`) was retired in objectui#8221 and now fails
validation."

Docs only, under `content/` (not a workspace package, not released): no
changeset. `node scripts/check-changeset-presence.mjs` agrees (verdict
line below).

## The retired-row form, and where it was copied from

The row copies the form the same file already uses for a live key whose
value lost a member: the `CalendarViewSchema` table's `view` row, which
keeps its row, narrows the Type column to what survives, and says
"`"agenda"` was retired in objectui#5740 and now fails validation." The
Type-column `*retired*` / `RETIRED` form (the `BaseSchema` `body` row,
the `related` row) is for a retired KEY; `sort` is live, so that form
does not apply.

## Premise check (objectui `origin/main` at `6c2f3c533`)

- The row read `string \| SortConfig[]`, as the card said.
- `@object-ui/types` declares `ObjectGridSchema.sort` as `SortConfig[]`
on the TypeScript face and as `z.array(SortConfigSchema)` on the zod
mirror (describe text: "array only; the legacy string clause is retired
— objectui#8221").
- The grid refuses a string at runtime: the `typeof schemaSort ===
'string'` arm in `ObjectGrid.tsx` ("the legacy string `sort` clause is
RETIRED (objectui#8221, decision batch objectstack-ai#77) and is REFUSED here rather
than lowered") sends no `$orderby`, and `convertSortToQueryParams`
reports the spelling once with `console.error`.
- "now fails validation" is an existing pin, not prose:
`packages/types/src/__tests__/sort-string-arm-retired-8221.test.ts`,
test "object-grid refuses the legacy string clause", asserts
`ObjectGridSchema.safeParse` fails on `sort: 'name desc'` with an issue
at path `sort`. It ran green at this head (below).

## Sweep of `content/`

No other row or example in `content/` authors a string `sort` on a
grid-family node. Searches run: quoted `field asc|desc` clauses; `sort:`
/ `"sort":` followed by a string or a string array; every `sort` table
row. What they found:

- the `ObjectGridSchema` example in this file already writes `"sort": [{
"field": "name", "order": "asc" }]`;
- `plugin-grid.mdx`, `plugin-map.mdx`, `plugin-gantt.mdx` and
`plugin-view.mdx` type or write `sort` as the array;
- the only string hits are not grid-family `sort`: `report-schema.mdx`
`groupBy[].sort: 'asc'` (the report grouping direction enum), and the
`$orderby` strings `'createdAt asc'` in `plugin-detail.mdx` and
`'createdAt desc'` in `data-objectstack.mdx` (`DataSource.find` query
params).

A repo-wide search for the union spelling `string | SortConfig` outside
`content/` matches only changesets, a CHANGELOG and test comments that
record the retirement.

## Gates (run locally at head `810b89da0`)

- `pnpm docs:check-links`, exit 0: "Links are valid across 17 scan
roots."
- `pnpm check:doc-fences`, exit 0: "check:doc-fences — every TypeScript
block in 227 document(s) is fenced ts/tsx/typescript …"
- `pnpm check:doc-example-ids`, exit 0: "414 real reference(s) all
resolve in the catalog registry."
- `pnpm check:doc-types`, exit 0: "Every documented component type is
registered."
- `node scripts/check-prompt-component-keys.mjs`, exit 0 (the same
workflow as `check:doc-types`).
- `node scripts/check-doc-expression-carriage.mjs`, exit 0 (Build Docs
job step; report-only census).
- `pnpm check:doc-example-readers`, exit 0; `pnpm check:spec-symbols`,
exit 0: "nothing cites a key its spec symbol does not declare."
- `node scripts/check-shell-escape-residue.mjs`, exit 0; `pnpm
check:docs-route-closure`, exit 0.
- `pnpm check:new-line-citations`, exit 0: "VERDICT
new-cross-file-line-citations: 0 new citation(s)".
- `pnpm check:control-bytes`, exit 0: "check-control-bytes: OK (scanned
8717 tracked text file(s) …)".
- `node scripts/check-changeset-presence.mjs`, exit 0: "No source or
published contract of a released package changed in this range, so no
changeset is owed." `check-changeset-claims`, exit 0 (report-only).
- `node scripts/check-governed-queue-guard.mjs --test
content/docs/api/schema-reference.md`: "NOT GOVERNED".
- Tests that read this file: `node scripts/markdown-test-inputs.mjs
--changed` names it, and its ledger names 18 test files (the
`content/docs/**` glob plus the two that read this file by name). Those
18 plus the objectui#8221 pin ran under the shared verify lock with
`pnpm exec vitest run --maxWorkers=2`: "Test Files 19 passed (19) ·
Tests 969 passed (969)", `VERDICT command-exit 0`.

No test pins this row's text (`Default sort configuration` has no hit
outside the row), so no test changed.

## Acceptance notes

- No new pin was added, so there is no red-then-green pair to show. The
"fails validation" claim rests on the existing objectui#8221 pin named
above.
- Declared narrowing, left to CI: `check:doc-snippets` / Doc Snippet
Types (it needs the packages' built `dist/*.d.ts` across a
near-whole-workspace build closure). Its population is fenced ts/tsx
blocks, and the edited row sits outside any fence (34 fence delimiters
come before it, an even count), so this diff cannot move that gate's
verdict. The Build Docs site build (`turbo run build
--filter=@object-ui/site`) and the full test shards are also CI's. No
workspace package is touched, so no package typecheck or test is owed.
- Observation only, not filed: the installed `@objectstack/spec@17.4.0`
still declares `ObjectGridPropsSchema.sort` as `z.unknown()`, and it
accepts `"name desc"` (measured with `safeParse`). objectstack `main`
already declares it as `z.array(SortItemSchema)`, and its describe text
refuses the string clause. The protocol face therefore catches up on the
next spec release that objectui installs. This row documents the
`@object-ui/types` face, which is the one this page covers.

Dev run `https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN`,
dispatched by the `domain:ui` seat 2 (claim comment 5832093380).

---
_Generated by [Claude
Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants