Repository navigation
Commit 55828c5
feat(spec): an empty state names its one next step (link: { label, url }), and a list page carries its own emptyState (#23098)
Fixes #23004
Fixes #23005
Clause-②: yes (widening: two new accepted keys)
The maintainer's "one plain sentence and one action" for an empty page
(objectstack-ai/cloud#2776, route letter A on objectstack-ai/cloud#2803,
contract first), as one fold of two cards (claim `6129995570`, member
claim `6130003856`). One commit per card, then the generated artifacts
and the changeset.
## What
**#23004 — `EmptyStateSchema.link`**
(`packages/spec/src/ui/view.zod.ts`, commit `ec742e938`)
- An empty state takes one optional, closed `link`: `{ label, url }`,
both required and nothing else. `label` is an `I18nLabel` (a string or
an inline locale map). A link only: the empty state invokes no action,
flow or script.
- `url` is judged by `navigationTargetRefusal` from
`navigation-target.ts`, the function a `type: 'url'` action's `target`
is judged by. It is reached by relative import, so there is no copy and
no second predicate. The subject is "An empty-state link's `url`". An
empty `url` is refused once, by `.min(1)` ("An empty-state link's `url`
is empty."), and the superRefine skips the empty string so the author
reads one issue.
- `EMPTY_STATE_ACTION_GUIDANCE` is restated. `action` / `button` now get
the link (`link` with `{ label, url }`, "It runs no action, flow or
script.") and keep the `addRecord` pointer. A flat `url` / `href` on the
empty state gets "`link: { label, url }`". The link carries aliases
`href`/`to`/`target` → `url` and `text`/`title` → `label`.
- The `link` describe carries `[PLANNED — not yet rendered]`. The view
ledger's `emptyState` row note records the exception.
**#23005 — `InterfacePageConfigSchema.emptyState`**
(`packages/spec/src/ui/page.zod.ts`, commit `477cedee5`)
- A `type: 'list'` page takes `interfaceConfig.emptyState:
EmptyStateSchema.optional()`, by reference: the list view's own
declaration, with no second shape. Its describe carries `[PLANNED — not
yet rendered]`, and the page ledger's `interfaceConfig` row note records
the exception.
## Measured calls (the dispatch's mechanism assumptions)
1. **Every door that carries `EmptyStateSchema`, on `origin/main`.**
`git grep EmptyStateSchema` finds two references:
`ListViewSchema.emptyState` (`view.zod.ts`) and
`ComponentPropsMap['object-grid'].emptyState` (`component.zod.ts`). This
PR adds a third, `InterfacePageConfigSchema.emptyState`. The build's
dropped-refinement census gives the published reach: 18 schemas, listed
under JSON Schema below. The translation group's
`_views.VIEW.emptyState` is a separate `{ title, message }` string
shape, so it is unaffected. Each door against objectui `origin/main`
`7af0ab3` (read-only `git show`):
| door | drawn today | the link / the page's own key |
|---|---|---|
| list view `emptyState` (and `listViews`, view documents, overlays) |
`title`/`message`/`icon`: `ListView.tsx:5688-5696` → `ListEmptyState` |
not read. Planned, carrier objectstack-ai/objectui#12270 item 1 |
| `object-grid` page block `emptyState` | `title`/`message`/`icon`:
`ObjectGrid.tsx:6647-6655` → `DataEmptyState` | not read. **No carrier
names it**: objectstack-ai/objectui#12270 lists `ListView.tsx` and
`InterfaceListPage.tsx` only (see Acceptance notes) |
| list page `interfaceConfig.emptyState` (new) | none:
`InterfaceListPage.tsx:503` passes the source view's `emptyState:
view.emptyState` | planned, carrier objectstack-ai/objectui#12270 item 2
|
Zero readers: in objectui at `7af0ab3`, `git grep` for an `emptyState`
`.link` read, an `emptyState.action` read or an `interfaceConfig`
`emptyState` read finds 0 real hits. The 1 hit is `emptyStateHint`, a
different key. Control: `emptyState` itself hits 5 / 3 / 1 times in
`ListView.tsx` / `ObjectGrid.tsx` / `InterfaceListPage.tsx`.
2. **The key name and the guidance.** A `guidance` entry is consulted
only for an undeclared key, and `alias-integrity.test.ts` holds every
entry to a key the shape refuses. A declared `action` key would have had
to drop its own guidance. So the key is `link`, as the consumer wrote it
("a link (`label` + `url`)"), and `action` / `button` stay guidance
entries, restated to point at it.
3. **Liveness: which mechanism.** It is the describe marker plus the
row's own `note`, the #23079 precedent. A gate-read `planned` row is not
possible without drilling. `view/list.emptyState` and
`page/interfaceConfig` are both recorded undrilled containers
(`undrilled-containers.baseline.json`), so the walk never reaches `link`
or `interfaceConfig.emptyState`, and a marker there is not read either.
Drilling `view/list.emptyState` would take four child rows (three `live`
with the objectui evidence above, `link` `planned`) and one baseline
shrink. That is outside the claimed file surface, so it is noted below
and not done. `check:liveness` exits 0.
4. **`check:empty-state`** is the pure source audit for permissive-empty
prose. It read the new describes and guidance and exits 0.
## JSON Schema: the link's grammar is a dropped refinement
`navigationTargetRefusal` is a function, so the rule is a `superRefine`,
and the published JSON Schema cannot express it. `link.url` publishes as
`type: string, minLength: 1`, and the position is named under
`x-dropped-refinements`. `dropped-refinements.baseline.json` gains the
sites exactly as the build printed them: 18 sites, with `ui/EmptyState`
a new entry (`link.url`), and one more site under each of `ui/ListView`,
`ui/ObjectListView`, `ui/View`, `ui/ViewItem`, `ui/ViewItemWire`,
`ui/ObjectGridProps`, `ui/InterfacePageConfig`, `ui/Page`,
`data/Object`, three `system/*Operation`/`ChangeSet` and five
`api/*Response`/package schemas. The `measured` counts stack with main's
#23062 (+1 schema, +2 sites): 228 schemas and 731 sites. The build
checks the sites, and it exits 0 at the head. A regex copy that would
project as `pattern` would be the second grammar the triage direction
rules out.
## Other files the change reaches (outside the claimed file surface:
stated, not silent)
- `packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`:
one `omit` ledger row for `page` `interfaceConfig.emptyState`. The gate
measured it: with the key added, "page.interfaceConfig: accepted by the
Zod but unauthorable in the form — offer it, or add a ledger entry:
expected [ 'emptyState' ]". The row is an omit, not a form control,
because no console list page draws the key yet, and a Studio control
would let an author write a value nothing draws. The row's reason says
it leaves with the renderer half.
- `packages/spec/dropped-refinements.baseline.json` (above), and the
generated artifacts `check:generated` proved stale:
`authorable-surface/ui.json` (`ui/EmptyState:link`,
`ui/InterfacePageConfig:emptyState`),
`content/docs/references/{ui/view,ui/page,ui/component,data/object,api/protocol}.mdx`,
and `docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md`
(one more strict site).
- No `api-surface/` change: the link schema is module-private
(`emptyStateLinkSchema`), so no new export.
## Readers of the two schemas: updated or unaffected
- Types: `EmptyState` (`z.input`) gains `link?: { label; url }`, and
`InterfacePageConfig` / `InterfacePageConfigParsed` gain `emptyState?`.
The superRefine adds no transform, so the
`type-alias-convention.pin.test.ts` isomorphism holds.
- `@objectstack/cli` `i18n-extract.ts` emits
`_views.VIEW.emptyState.{title,message}` and is unaffected (see
Acceptance notes for `link.label`).
- `@objectstack/lint` (`showcase-shape.fixtures.ts`, the page walkers),
`@objectstack/platform-objects` and `examples/app-showcase` author
`emptyState` with `title`/`message`/`icon`, or list pages without it.
All are unaffected, and their suites ran below.
## Verification
All commands ran under `scripts/pm/os-verify-lock.sh`, with exit codes
captured before any pipe. The final head is `eb4d7de12`, which is this
branch with `origin/main` `6662e3f59` merged in. Two merges were made
through `scripts/pm/os-regen-merge.sh`. The first brought #23080, and
its two driver-dropped artifacts were regenerated in `a449f542c`. The
second brought #23062, and its `dropped-refinements.baseline.json`
conflict was settled in `eb4d7de12` by stacking the counts. The net
delta against `origin/main` is still these 16 files, +604/−25.
- **Spec build and generated artifacts at `eb4d7de12`:** `pnpm --filter
@objectstack/spec build` exit 0. `check:generated` exit 0: "All 14
generated artifacts are up to date".
- **New pins** (84 tests, all passing):
- `src/ui/empty-state-link.test.ts` (61): §1 accepted forms,
byte-identical. §2 one refusal per grammar class, first sentence + path
`link.url` + code `custom`, and the empty url as `too_small`. §3 one
grammar: on 27 targets the link's verdict and reason equal a registered
`type: 'url'` action's, subject set aside. §4 malformed links: no label
→ `invalid_union` at `link.label`; no url / numeric url / bare string /
array → `invalid_type`; extra key / an `action` on the link →
`unrecognized_keys` at `link`. §5 guidance. §6 the list-view and
`object-grid` doors answer what the schema answers.
- `src/ui/page-interface-empty-state.test.ts` (23): accepted on
`InterfacePageConfigSchema` and through a whole `type: 'list'` page.
Refused by code + path, at `emptyState…` and at
`interfaceConfig.emptyState…`. A lit unknown-key control on the config
itself. Identity: the member unwraps to `EmptyStateSchema._zod.def`,
plus a corpus parity check.
- **Targeted spec files at `eb4d7de12`:** the two pin files plus
`metadata-form-zod-reconciliation`, `alias-integrity`,
`component-object-grid-declared-keys.pin`, `view-strictness-batch18`,
`type-alias-convention.pin`, `page`, `action-url-target-scheme-set`. 9
files, 474 passed.
- **Full spec suite at `a449f542c`**, before the second merge, whose
incoming commits touch `src/automation/**` and the baseline counts only:
`pnpm --filter @objectstack/spec test` 659 files, 19950 passed. `pnpm
--filter @objectstack/spec typecheck` exit 0, including
`check:test-typecheck` ("52 file(s) / 246 error(s) / 135 pinned
signature(s) held", so the new test files compile with no new debt). At
`5bbc19059`, before both merges: 658 files, 19933 passed.
- **Consumer suites at `5bbc19059`** (the same net diff, older base):
- `@objectstack/lint` 137 files, 6569 passed.
- `@objectstack/platform-objects` 70 files, 1096 passed.
- `@objectstack/example-showcase` 33 files, 409 passed.
- `@objectstack/cli` `vitest run --project unit` 284 files, 4276 passed.
- The cli `integration` tier is declared to CI. The diff touches no
spawn entry.
- **Reverse type check against the rebuilt `dist/ui/index.d.mts`.** A
scratch program typed `EmptyState` / `InterfacePageConfig`. With `link:
{ label, url, openIn }` it exits 2 with `TS2353: Object literal may only
specify known properties, and 'openIn' does not exist in type '{ label:
…; url: string; }'`. With that line removed it exits 0, with a list-view
link and a page `emptyState` link both compiling.
- **Gate families:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` (no paths) derived 110 families at
`eb4d7de12`, against merge base `6662e3f59`: the same list as at
`5bbc19059`. All 110 were run at `eb4d7de12`. 109 exited 0 and 1 exited
1. `--ran` reconciliation: "110 derived, 110 run, 0 NOT-MEASURED, 0
UNRUN". The red one is `pnpm check:platform-checklist`: 8 `ABSENT
SYMBOL` / anchor-floor problems in
`docs/qa/platform-checklist/areas/{access-security,attachments-storage,search}.json`,
which this diff does not touch. Control leg: the same command in a clean
detached worktree at `origin/main` `6662e3f59` exits 1 with the
identical 8 lines (`diff` empty), as it also did at `8c1077615`. In the
first battery at `5bbc19059`, `check:skill-examples` and
`check:dual-build-cjs-loads` refused with exit 3 (unbuilt `client-react`
and 8 other packages). They exited 0 after those packages were built,
and both exited 0 in the battery at `eb4d7de12`.
- **Control bytes:** `grep -naP` for raw control bytes over the 16
changed files finds none. The tab / line feed / carriage-return cases
are escapes in source. `pnpm check:nul-bytes` exit 0.
## Acceptance notes
Recorded here, not filed:
- **The `object-grid` door accepts `link`, and no renderer card names
it.** objectstack-ai/objectui#12270 lists `ListView.tsx` (item 1) and
`InterfaceListPage.tsx` (item 2). `ObjectGrid.tsx:6647-6655`, the
`object-grid` page block's own empty state, is not on it. Carrier: the
seat, by adding it to objectstack-ai/objectui#12270's scope.
- **The `object-grid` `emptyState` describe still reads `{ title,
message, icon }`** (`component.zod.ts`). It is not wrong about what the
grid draws, but it lists three of four accepted members.
`component.zod.ts` belonged to #23080 in this round, so it is untouched
here. Carrier: the next PR that touches that describe, or the
objectstack-ai/objectui#12270 follow-up when the grid draws the link.
- **`link.label` has no translation-bundle address.**
`_views.VIEW.emptyState` declares `title` / `message` only, and `os i18n
extract` emits those two only. The inline locale map is the label's
route, and the page-prose route the translation file already names.
Carrier: none.
- **The `${…}` arm.** The grammar admits a target that opens with `${`,
judged by the runner after interpolation, because the url action's set
includes it. That holds only if the renderer opens the link through the
action runner, which objectstack-ai/objectui#12270 item 1 already asks.
Carrier: objectstack-ai/objectui#12270.
- **When the renderer half lands:** the two `[PLANNED — not yet
rendered]` markers leave, the two ledger notes are rewritten,
`view/list.emptyState` can be drilled into a gate-read row, and the page
form offers `emptyState` with the reconciliation `omit` row deleted.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent abf8655 commit 55828c5
16 files changed
Lines changed: 604 additions & 25 deletions
File tree
- .changeset
- content/docs/references
- api
- data
- ui
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages/spec
- authorable-surface
- liveness
- src
- system
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1731 | 1731 | | |
1732 | 1732 | | |
1733 | 1733 | | |
1734 | | - | |
| 1734 | + | |
1735 | 1735 | | |
1736 | 1736 | | |
1737 | 1737 | | |
| |||
1817 | 1817 | | |
1818 | 1818 | | |
1819 | 1819 | | |
1820 | | - | |
| 1820 | + | |
1821 | 1821 | | |
1822 | 1822 | | |
1823 | 1823 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
410 | 410 | | |
411 | 411 | | |
412 | 412 | | |
413 | | - | |
| 413 | + | |
414 | 414 | | |
415 | 415 | | |
416 | 416 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
793 | 793 | | |
794 | 794 | | |
795 | 795 | | |
796 | | - | |
| 796 | + | |
797 | 797 | | |
798 | 798 | | |
799 | 799 | | |
| |||
837 | 837 | | |
838 | 838 | | |
839 | 839 | | |
| 840 | + | |
840 | 841 | | |
841 | 842 | | |
842 | 843 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
| 102 | + | |
102 | 103 | | |
103 | 104 | | |
104 | 105 | | |
| |||
181 | 182 | | |
182 | 183 | | |
183 | 184 | | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
184 | 194 | | |
185 | 195 | | |
186 | 196 | | |
| |||
250 | 260 | | |
251 | 261 | | |
252 | 262 | | |
| 263 | + | |
253 | 264 | | |
254 | 265 | | |
255 | 266 | | |
| |||
0 commit comments