Repository navigation
feat(spec): an empty state names its one next step (link: { label, url }), and a list page carries its own emptyState - #23098
Conversation
… gains link { label, url }
EmptyStateSchema gains one optional, closed `link` ({ label, url }, both
required). `url` is judged by navigationTargetRefusal, the one grammar a
type: 'url' action's target is judged by, so the door admits exactly what
the console's action runner opens; an empty url is refused once by
minLength. A link only: the empty state invokes no action.
EMPTY_STATE_ACTION_GUIDANCE is restated to point `action` / `button` at
the link (keeping the addRecord pointer), and a flat `url` / `href` is
pointed into the link block. The describe carries the planned marker and
the view ledger's emptyState note records the exception; the renderer
half is objectstack-ai/objectui#12270.
Claude-Session: https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ
Co-authored-by: Claude <noreply@anthropic.com>
…figSchema.emptyState InterfacePageConfigSchema gains an optional `emptyState`: the list view's EmptyStateSchema, taken by reference (no second empty-state shape), so a type: 'list' page can say what belongs in an empty list, with the same title / message / icon and one link a list view's empty state carries. The describe carries the planned marker and the page ledger's interfaceConfig note records the exception. The page form does not offer the key yet: the form/Zod reconciliation ledger gains an omit row with the reason (no console list page draws it yet), which leaves with the renderer half, objectstack-ai/objectui#12270. Claude-Session: https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ Co-authored-by: Claude <noreply@anthropic.com>
…ement; changeset for both keys - dropped-refinements.baseline.json: the link's url grammar is a refinement the published JSON Schema cannot express; one new site per schema that reaches EmptyStateSchema (18 sites, ui/EmptyState a new entry), applied exactly as the build printed them. - authorable-surface/ui.json (gen:schema in build), content/docs/references and the strictness-ledger ui counts (check:generated --fix, the two it proved stale). - .changeset/23004-empty-state-link.md: minor, Clause-② yes (widening). Claude-Session: https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ Co-authored-by: Claude <noreply@anthropic.com>
… not text-merge The os-regen driver kept one side of component.mdx and the strictness ledger ui counts; both re-derived from the merged tree (spec build, then gen:strictness-ledger and gen:docs): #23080's record:details sections count row and this branch's empty-state link row, ui sites 214. Claude-Session: https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ Co-authored-by: Claude <noreply@anthropic.com>
…pty-state-link dropped-refinements.baseline.json: both sides added sites; the measured counts stack (main +1 schema / +2 sites, this branch +1 / +18): 228 schemas, 731 sites. Claude-Session: https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 140 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 597e32722a2e58778bfe4ec538f091b81a66e19f && git checkout 597e32722a2e58778bfe4ec538f091b81a66e19f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6662e3f590e9790a28f598d3eb6f665e46f10703 5adfbc1411507f5dc80f9004b4902a5f8031663a && git checkout -B drift-repro 6662e3f590e9790a28f598d3eb6f665e46f10703 && git merge --no-ff 5adfbc1411507f5dc80f9004b4902a5f8031663a
node scripts/docs-audit/affected-docs.mjs --json 6662e3f590e9790a28f598d3eb6f665e46f10703 |
…rl describe
The `link.url` describe wrote a lone `${` inside an inline-code span, and
gen:docs copied it into content/docs/references/ui/view.mdx, which the
escape-mdx test refuses (an inline-code span with an unbalanced brace).
Reworded to "a template that opens with `${…}`, such as
`${ctx.origin}/docs`", same meaning; the changeset's matching sentence is
balanced the same way.
Claude-Session: https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ
Co-authored-by: Claude <noreply@anthropic.com>
…be fix check:generated --fix, the one artifact it proved stale. Claude-Session: https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: PR #23098, the fold of #23004 and #23005 on branch ① Derived judgments
Checked and acceptable without a line of its own: ② Semver level
③ Boundary flagsThe dev report (
Nothing escalates to the maintainer. Implemented-by: VERDICT: PASS |
Step 4 of os-regen-merge: the two os-regen mdx files regenerated from the merge of origin/main 55828c5, carrying #23098's and #23075's rows and this card's member rows. Claude-Session: https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ Co-authored-by: Claude <noreply@anthropic.com>
…onent reference The merge took main's ledger side whole (228 schemas / 731 sites, #23098's 18 emptyState.link.url sites among them). build-schemas on the merged tree then printed this branch's 21 sites as undeclared or moved: 4 new entries and 17 sites in 13 existing entries. They are applied exactly as printed, and the header totals now match the body: 232 schemas / 752 sites. gen:docs regenerates ui/component.mdx from the merged tree. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
…ion runner, and let a list page's own emptyState win (objectstack-ai#12270) (objectstack-ai#12350) Fixes objectstack-ai#12270 Clause-②: yes. The renderers start honouring two keys the spec declares (`emptyState.link`, and a list page's own `interfaceConfig.emptyState`), so what they draw widens. A contract review is owed before the queue. ## What this does This is the renderer half of objectstack-ai/objectstack#23004 and objectstack-ai/objectstack#23005 (spec PR objectstack-ai/objectstack#23098, merge `55828c5145`). It is unblocked because `main` `c5406c2` installs `@objectstack/spec` `18.0.0-next.0`. The change covers three doors, which share one piece: - **The shared piece.** `EmptyStateLink`, in `@object-ui/components` beside `DataEmptyState`, draws the spec's `EmptyStateSchema.link` (`{ label, url }`). It follows the link as a `type: 'url'` action through the ActionRunner, so what a click opens is `classifyNavigationTarget`'s answer. It has no scheme list or URL check of its own. The `label` is a string or an inline locale map, resolved against the caller's display locale. A label that resolves to no text draws nothing. - **Door 1: `ListView`'s empty branch** (`grid` and `gallery`). `ListEmptyState` takes `link` and draws it beside the add-record action. The spec keeps the two apart, so either, both or neither may be drawn. With no link, the DOM is unchanged. - **Door 2: `InterfaceListPage`.** The relay is now `emptyState: cfg.emptyState ?? view.emptyState`. The page's own block wins, whole, on every visualization, because the precedence is set before the visualization is chosen. The source view's block is the fallback, as its `appearance` and `userFilters` already are. - **Door 3: `ObjectGrid`'s authored empty state.** It draws the link in `DataEmptyState`'s action slot. The registry `inputs` description for `object-grid.emptyState` now names `link`. Docs: the `emptyState` passages in the plugin-grid docs, README and schema reference now cover `link`. The plugin-list README gains an "Empty state" section; its typed example is checked below. Changeset: `minor` for the four packages touched. **For the spec seat (scope addition `6132635356`):** the renderer half has landed with this PR. The follow-up is the spec seat's to file: dropping the two `[PLANNED — not yet rendered]` markers, rewriting the two ledger notes, offering `interfaceConfig.emptyState` in the page form and deleting its reconciliation omit row. ## How a declared link becomes a runner navigation (measured with the real runner) | target | under the console's provider (`onNavigate` + `onToast`) | no provider | |---|---|---| | in-app `/...` or `./...` | `onNavigate(url, { external: false, newTab: false })`, in place | the runner hands back `redirect`; followed with `location.assign` | | `https://...` | `onNavigate(url, { external: true, newTab: true })` | `window.open(url, '_blank', 'noopener,noreferrer')` | | `mailto:` | `location.href = url`, in place; router and new tab untouched | the same | | refused (`//host`, `thanks`, `javascript:`) | the runner's own refusal is toasted as an error; no navigation sink is touched | the refusal is logged; no navigation | A followed link raises no success toast. The html-tier anchor and the dashboard header link raise none either. ## Deviation from the suggested route: a refused value is refused at the click, not hidden at render The suggested route said "a refused value draws no link". Here the link is drawn, and the runner refuses it at the click and reports its refusal. Reason, measured: the spec admits at parse a url that starts with a `${...}` template, and its describe says such a url "is judged when it is clicked, after interpolation". `EmptyStateSchema.safeParse` of a link whose url is `${ctx.origin}/docs` succeeds on `18.0.0-next.0`. But `classifyNavigationTarget` answers `null` for that string before interpolation. Classifying at render would therefore hide a spec-valid link, and it would also be a second judgement beside the runner's. The pin `judges nothing at render` holds this. ## Mechanism assumptions, measured 1. **Installed shapes.** `EmptyStateSchema.link` is strict `{ label: I18nLabel, url: string }`: `url` has `min(1)` and `navigationTargetRefusal` at parse. `InterfacePageConfigSchema.emptyState` is `EmptyStateSchema.optional()`. The hold's restart probe exits 0 from `apps/console`. 2. **At `c5406c2`.** `ListView` handed `ListEmptyState` `title`, `message` and `icon` only, and `ObjectGrid` read the same three. `InterfaceListPage` relayed the source view's whole block (`emptyState: view.emptyState`) and read no `cfg.emptyState`; it did not read individual members. `classifyNavigationTarget` is the runner's one classifier (`navigateTo`, `navigateOnSuccess`). 3. **`@object-ui/types` mirrors: none needed.** `ListViewSchema['emptyState']` and `ObjectGridSchema.emptyState` both take the spec's `EmptyState` by reference. The README example (a `ListViewSchema` literal with `link: { label, url }`) compiles against the built `@object-ui/types`. The same literal with an undeclared `href` inside the link is refused with TS2353. ## Tests (real renders, real runner) - `packages/components/src/__tests__/empty-state-link-12270.test.tsx` (14). Under a provider: in-app, https, mailto, three refused values (each compared with what a bare runner answers for the same target), a `${...}` template drawn, the label map, and absent or empty label. With no provider: redirect followed, https in a new tab, refusals logged. - `packages/plugin-list/src/__tests__/ListView.emptyStateLink-12270.test.tsx` (4). Grid: link beside add-record, followed in place. Gallery: https. A refused value. A control with no link. - `packages/plugin-grid/src/__tests__/ObjectGrid.emptyStateLink-12270.test.tsx` (4). Through the registry and `SchemaRenderer`: in-app, https, refused, and a control with no link. - `packages/app-shell/src/views/InterfaceListPage.emptyStatePrecedence-12270.test.tsx` (6). The real page renders the real `ListView`. The page's own sentence and link win on grid and on gallery, whole (the view's message does not fill the gap). The page's link is followed in place. With no page block, the view's is kept, on grid and on gallery. A control proves the gallery page mounts the gallery (cards, no table). ## Ablation (three legs) Each leg went through `ablation-replace.mjs`: the anchor hit exactly once, the blob changed, and the restore was proven by blob == HEAD and an empty `git diff HEAD`. The direction was red, as predicted. - `InterfaceListPage`: `cfg.emptyState ?? view.emptyState` became `view.emptyState`. 3 failed, 3 passed: the three page-precedence rows failed; the two fallback rows and the gallery control passed. - `ListView`: `link={schema.emptyState?.link}` became `link={undefined}`. 3 failed, 1 passed: the control passed. - `ObjectGrid`: the link's `action` slot was emptied. 3 failed, 1 passed: the control passed. ## Local verification (HEAD `f20667b0e`) - Closure build: `turbo run build --filter=@object-ui/plugin-list^... --filter=@object-ui/plugin-grid^... --filter=@object-ui/app-shell^...`, plus the two plugins: 28/28 tasks. - Suites (run from the repo root, path-filtered, through the verify lock): - `packages/plugin-list/`: 131 files, 1419 tests passed. - `packages/plugin-grid/`: 211 files, 1920 tests passed. - `packages/components/`: 382 files passed, 1 skipped; 3875 tests passed, 24 skipped. - The 27 app-shell files that name `InterfaceListPage` or `emptyState`: 273 tests passed. - `type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`; each `tsconfig.test.json` includes `src/**/*.test.tsx`): components, plugin-list and plugin-grid all exit 0. - app-shell `type-check`, whole package: NOT MEASURED. Reason: the verify-lock queue had not reached it after about 30 minutes, behind another seat's long locked run (`issue-11773` `final2.sh`). It is declared here as a narrowing, and CI runs the whole package. The narrowed check: `tsc` with the package's own `tsconfig.test.json`, whose `files` are the three touched app-shell files (the new test, `InterfaceListPage.tsx` and the census test). That program spans 629 workspace files, 79 of them under `app-shell/src`, and exits 0. - eslint on the 12 touched TS files, with the repo config. The population comes from eslint's own JSON output: 12 files, 0 errors. Warning counts are unchanged on every touched source file, base vs head. The 5 warnings in the new app-shell test are the `no-explicit-any` mock pattern its siblings use. The narrowing is sound: the config enables no type-aware linting (no `project` / `projectService`), and no local rule reads another file, so this diff cannot move the verdict on an untouched file. - Repo checks, all exit 0: `check-changeset-presence`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:new-line-citations`, `check:control-bytes`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check-doc-links`, `check:doc-json`, `check:doc-fences`, `check:doc-example-ids`, `check:doc-types`, `check:unreferenced-sources`, `check:phantom-deps`, `check:self-import`, `check:action-forward-parity`, `check:handler-key-reads`, `check:spec-symbols`, `check:esm-specifiers`, `check:i18n-keys`, `check-changeset-no-major`, `check-changeset-fixed` and `check-changeset-overwrite`. No string or i18n key was added, so the rest of the `check:i18n-*` family is not owed. - NOT MEASURED: `check:doc-snippets` and `check:readme-exports`. Reason: PRECONDITION NOT MET, because packages outside this closure are unbuilt (plugin-gantt, plugin-map, plugin-markdown, plugin-timeline and others). CI builds them. The judged rows of `readme-exports` show 0 fabricated names. The new README snippet was compiled on its own (item 3 above). ## Acceptance notes - `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`, in the `MEMBER_PINS` row `object-grid.emptyState`, still says "All three members the spec's strict `EmptyStateSchema` declares", but spec 18 declares four. It is mechanically green, because the check reads only that the pin file names the block and the key. The `link` member's pin is `ObjectGrid.emptyStateLink-12270.test.tsx`. This is prose drift, not filed. Carrier: whoever next edits that ledger row, for example the spec seat's follow-up above. - The empty state, and so its link, is drawn on the `grid` and `gallery` visualizations only, as before. The other visualizations draw their own empty surfaces. That is unchanged here; the page precedence holds whatever the visualization. - CI: `Spec Main Shape Gate` is red on `main` and on every PR (objectstack-ai/objectstack#23160; consumer objectstack-ai#12325). It is advisory and not this change's. `OAuthConsentPage.public-client-12301.test.tsx` is a known flake (objectstack-ai#12344). --- _Generated by [Claude Code](https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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 claim6130003856). One commit per card, then the generated artifacts and the changeset.What
#23004 —
EmptyStateSchema.link(packages/spec/src/ui/view.zod.ts, commitec742e938)link:{ label, url }, both required and nothing else.labelis anI18nLabel(a string or an inline locale map). A link only: the empty state invokes no action, flow or script.urlis judged bynavigationTargetRefusalfromnavigation-target.ts, the function atype: 'url'action'stargetis judged by. It is reached by relative import, so there is no copy and no second predicate. The subject is "An empty-state link'surl". An emptyurlis refused once, by.min(1)("An empty-state link'surlis empty."), and the superRefine skips the empty string so the author reads one issue.EMPTY_STATE_ACTION_GUIDANCEis restated.action/buttonnow get the link (linkwith{ label, url }, "It runs no action, flow or script.") and keep theaddRecordpointer. A flaturl/hrefon the empty state gets "link: { label, url }". The link carries aliaseshref/to/target→urlandtext/title→label.linkdescribe carries[PLANNED — not yet rendered]. The view ledger'semptyStaterow note records the exception.#23005 —
InterfacePageConfigSchema.emptyState(packages/spec/src/ui/page.zod.ts, commit477cedee5)type: 'list'page takesinterfaceConfig.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'sinterfaceConfigrow note records the exception.Measured calls (the dispatch's mechanism assumptions)
Every door that carries
EmptyStateSchema, onorigin/main.git grep EmptyStateSchemafinds two references:ListViewSchema.emptyState(view.zod.ts) andComponentPropsMap['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.emptyStateis a separate{ title, message }string shape, so it is unaffected. Each door against objectuiorigin/main7af0ab3(read-onlygit show):emptyState(andlistViews, view documents, overlays)title/message/icon:ListView.tsx:5688-5696→ListEmptyStateobject-gridpage blockemptyStatetitle/message/icon:ObjectGrid.tsx:6647-6655→DataEmptyStateListView.tsxandInterfaceListPage.tsxonly (see Acceptance notes)interfaceConfig.emptyState(new)InterfaceListPage.tsx:503passes the source view'semptyState: view.emptyStateZero readers: in objectui at
7af0ab3,git grepfor anemptyState.linkread, anemptyState.actionread or aninterfaceConfigemptyStateread finds 0 real hits. The 1 hit isemptyStateHint, a different key. Control:emptyStateitself hits 5 / 3 / 1 times inListView.tsx/ObjectGrid.tsx/InterfaceListPage.tsx.The key name and the guidance. A
guidanceentry is consulted only for an undeclared key, andalias-integrity.test.tsholds every entry to a key the shape refuses. A declaredactionkey would have had to drop its own guidance. So the key islink, as the consumer wrote it ("a link (label+url)"), andaction/buttonstay guidance entries, restated to point at it.Liveness: which mechanism. It is the describe marker plus the row's own
note, the feat(spec): a page element dataSource can name an API endpoint (provider: 'api') #23079 precedent. A gate-readplannedrow is not possible without drilling.view/list.emptyStateandpage/interfaceConfigare both recorded undrilled containers (undrilled-containers.baseline.json), so the walk never reacheslinkorinterfaceConfig.emptyState, and a marker there is not read either. Drillingview/list.emptyStatewould take four child rows (threelivewith the objectui evidence above,linkplanned) and one baseline shrink. That is outside the claimed file surface, so it is noted below and not done.check:livenessexits 0.check:empty-stateis 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
navigationTargetRefusalis a function, so the rule is asuperRefine, and the published JSON Schema cannot express it.link.urlpublishes astype: string, minLength: 1, and the position is named underx-dropped-refinements.dropped-refinements.baseline.jsongains the sites exactly as the build printed them: 18 sites, withui/EmptyStatea new entry (link.url), and one more site under each ofui/ListView,ui/ObjectListView,ui/View,ui/ViewItem,ui/ViewItemWire,ui/ObjectGridProps,ui/InterfacePageConfig,ui/Page,data/Object, threesystem/*Operation/ChangeSetand fiveapi/*Response/package schemas. Themeasuredcounts 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 aspatternwould 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: oneomitledger row forpageinterfaceConfig.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 artifactscheck:generatedproved 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, anddocs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md(one more strict site).api-surface/change: the link schema is module-private (emptyStateLinkSchema), so no new export.Readers of the two schemas: updated or unaffected
EmptyState(z.input) gainslink?: { label; url }, andInterfacePageConfig/InterfacePageConfigParsedgainemptyState?. The superRefine adds no transform, so thetype-alias-convention.pin.test.tsisomorphism holds.@objectstack/clii18n-extract.tsemits_views.VIEW.emptyState.{title,message}and is unaffected (see Acceptance notes forlink.label).@objectstack/lint(showcase-shape.fixtures.ts, the page walkers),@objectstack/platform-objectsandexamples/app-showcaseauthoremptyStatewithtitle/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 iseb4d7de12, which is this branch withorigin/main6662e3f59merged in. Two merges were made throughscripts/pm/os-regen-merge.sh. The first brought #23080, and its two driver-dropped artifacts were regenerated ina449f542c. The second brought #23062, and itsdropped-refinements.baseline.jsonconflict was settled ineb4d7de12by stacking the counts. The net delta againstorigin/mainis still these 16 files, +604/−25.eb4d7de12:pnpm --filter @objectstack/spec buildexit 0.check:generatedexit 0: "All 14 generated artifacts are up to date".src/ui/empty-state-link.test.ts(61): §1 accepted forms, byte-identical. §2 one refusal per grammar class, first sentence + pathlink.url+ codecustom, and the empty url astoo_small. §3 one grammar: on 27 targets the link's verdict and reason equal a registeredtype: 'url'action's, subject set aside. §4 malformed links: no label →invalid_unionatlink.label; no url / numeric url / bare string / array →invalid_type; extra key / anactionon the link →unrecognized_keysatlink. §5 guidance. §6 the list-view andobject-griddoors answer what the schema answers.src/ui/page-interface-empty-state.test.ts(23): accepted onInterfacePageConfigSchemaand through a wholetype: 'list'page. Refused by code + path, atemptyState…and atinterfaceConfig.emptyState…. A lit unknown-key control on the config itself. Identity: the member unwraps toEmptyStateSchema._zod.def, plus a corpus parity check.eb4d7de12: the two pin files plusmetadata-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.a449f542c, before the second merge, whose incoming commits touchsrc/automation/**and the baseline counts only:pnpm --filter @objectstack/spec test659 files, 19950 passed.pnpm --filter @objectstack/spec typecheckexit 0, includingcheck:test-typecheck("52 file(s) / 246 error(s) / 135 pinned signature(s) held", so the new test files compile with no new debt). At5bbc19059, before both merges: 658 files, 19933 passed.5bbc19059(the same net diff, older base):@objectstack/lint137 files, 6569 passed.@objectstack/platform-objects70 files, 1096 passed.@objectstack/example-showcase33 files, 409 passed.@objectstack/clivitest run --project unit284 files, 4276 passed.integrationtier is declared to CI. The diff touches no spawn entry.dist/ui/index.d.mts. A scratch program typedEmptyState/InterfacePageConfig. Withlink: { label, url, openIn }it exits 2 withTS2353: 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 pageemptyStatelink both compiling.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 110 families ateb4d7de12, against merge base6662e3f59: the same list as at5bbc19059. All 110 were run ateb4d7de12. 109 exited 0 and 1 exited 1.--ranreconciliation: "110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN". The red one ispnpm check:platform-checklist: 8ABSENT SYMBOL/ anchor-floor problems indocs/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 atorigin/main6662e3f59exits 1 with the identical 8 lines (diffempty), as it also did at8c1077615. In the first battery at5bbc19059,check:skill-examplesandcheck:dual-build-cjs-loadsrefused with exit 3 (unbuiltclient-reactand 8 other packages). They exited 0 after those packages were built, and both exited 0 in the battery ateb4d7de12.grep -naPfor raw control bytes over the 16 changed files finds none. The tab / line feed / carriage-return cases are escapes in source.pnpm check:nul-bytesexit 0.Acceptance notes
Recorded here, not filed:
object-griddoor acceptslink, and no renderer card names it. Render a list's declared empty-state link, and let a list page's own emptyState win over its source view's (the objectui half of objectstack#23004 and objectstack#23005) objectui#12270 listsListView.tsx(item 1) andInterfaceListPage.tsx(item 2).ObjectGrid.tsx:6647-6655, theobject-gridpage block's own empty state, is not on it. Carrier: the seat, by adding it to Render a list's declared empty-state link, and let a list page's own emptyState win over its source view's (the objectui half of objectstack#23004 and objectstack#23005) objectui#12270's scope.object-gridemptyStatedescribe 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.tsbelonged to feat(spec): a record:details section heading can show how many values a multi-value field holds (sections[].count and the {{ count }} hole) #23080 in this round, so it is untouched here. Carrier: the next PR that touches that describe, or the Render a list's declared empty-state link, and let a list page's own emptyState win over its source view's (the objectui half of objectstack#23004 and objectstack#23005) objectui#12270 follow-up when the grid draws the link.link.labelhas no translation-bundle address._views.VIEW.emptyStatedeclarestitle/messageonly, andos i18n extractemits those two only. The inline locale map is the label's route, and the page-prose route the translation file already names. Carrier: none.${…}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 Render a list's declared empty-state link, and let a list page's own emptyState win over its source view's (the objectui half of objectstack#23004 and objectstack#23005) objectui#12270 item 1 already asks. Carrier: Render a list's declared empty-state link, and let a list page's own emptyState win over its source view's (the objectui half of objectstack#23004 and objectstack#23005) objectui#12270.[PLANNED — not yet rendered]markers leave, the two ledger notes are rewritten,view/list.emptyStatecan be drilled into a gate-read row, and the page form offersemptyStatewith the reconciliationomitrow deleted.Generated by Claude Code