Commit 1b82c51
feat(spec): declare the author-settable row ceiling for the page-shaped view configs (#19226)
Fixes #17393
Clause-②: yes (widening)
`GalleryConfigSchema`, `KanbanConfigSchema` and `TimelineConfigSchema`
each gain a `limit`
member — `z.number().int().positive().default(100)` — and
`DEFAULT_VIEW_ROW_LIMIT` is exported
beside them. The key's own text states both halves of the contract: the
default it applies, and
that the renderer must show a visible truncation signal when the ceiling
applies.
## The one design call the card delegates: which shape, and why
**Chosen: a shared `limit` on the three page-shaped config blocks.
Rejected: a member on the
base view config (`ListViewShapeSchema`).** Three properties of this
tree decide it, not taste:
1. **The base shape already carries the row-bounding knob every view
type reaches** —
`pagination.pageSize` (`PaginationConfigSchema`, default 25). A second
base-level row key
would leave one view with two base-level row bounds and no declared
precedence between them,
and the `virtualScroll` tombstone at the bottom of that same shape
already prescribes
`pagination` for exactly that question ("large datasets page via
`pagination`").
2. **A base member is reachable from every `type`** — the non-grid four
(gantt / calendar / map
/ tree) included. Their ceiling is a platform constant the renderer owns
(objectui#7210) and
this card scopes them out by name, so a base member would publish an
authorable ceiling on
four view kinds no renderer reads: declared-but-unenforced on the day it
lands.
3. **The per-kind block is what actually reaches the renderer.**
Measured in objectui at
`dda8f3815d`: `ListView`'s kanban branch destructures the merged block
and spreads the rest
flat onto the generated `object-kanban` node
(`packages/plugin-list/src/ListView.tsx`, the
`...restKanban` in that branch's return), so a protocol `kanban.limit`
lands exactly where
`ObjectKanban.tsx:573` already reads `schema.limit`. A base-level key is
forwarded into no
per-kind node at all.
The NAME follows the same evidence: `limit` is the name the consumers
already read, so this
declaration absorbs the two consumer-local keys instead of buying a
second spelling.
## The default, and the truncation signal
The default is **applied**, not merely described. A `.describe()` naming
a default the schema
does not apply is a second contract nothing enforces, so the two are
pinned to each other: the
test parses each minimal block, reads the number out of the member's own
describe text, and
asserts they are the same value. Change one without the other and the
case reddens.
The truncation signal cannot be enforced from a schema — it is the
renderer's half. What the
protocol can do is say it is owed, which is what the describe text does,
and a pin asserts the
sentence is there. That sentence is the one the objectui#7390 dispatch
turns on: adding `$top`
without a signal trades "unbounded and silent" for "bounded and silent",
which is worse,
because the user then believes they are seeing everything.
## The consumer-local keys, measured here rather than repeated from the
card
Read-only measurement of objectui at its current head `dda8f3815d` (this
card touches that repo
in no way):
- **kanban** — `ObjectKanbanSchema.limit`, declared in
`@object-ui/types` alone
(`packages/types/src/zod/objectql.zod.ts:1762`,
`z.number().int().positive().optional()`,
describe "default 100 (DEFAULT_KANBAN_LIMIT)"); read at
`packages/plugin-kanban/src/ObjectKanban.tsx:573` as `$top: schema.limit
?? DEFAULT_KANBAN_LIMIT`,
with that constant `= 100` at `:84`.
- **timeline** — `limit?: number` on `ObjectTimeline`'s own props
interface
(`packages/plugin-timeline/src/ObjectTimeline.tsx:129`) and on no
published schema at all;
read at `:328` as `$top: schema.limit ?? DEFAULT_TIMELINE_LIMIT`, that
constant `= 100` at `:29`.
- **gallery** — zero `$top` and zero `limit` in
`packages/plugin-list/src/ObjectGallery.tsx`:
the unbounded fetch objectui#7390 is ruled to close by reading an
author-settable ceiling.
So the name this PR chose is the one the consumer already reads, at the
same type (int,
positive), and the number it declares — 100 — is the value both existing
renderer constants
carry, which is what the ruling asked the implementing seat to align
`DEFAULT_GALLERY_LIMIT`
with. The card's claim was re-derived, not inherited, and it holds.
## Ablation — three runs, each restored byte-clean
Every leg ran through `scripts/ablation-replace.mjs`, which proves the
write on disk (anchor
count, replacement count, blob hash) and proves the restore against
`HEAD` rather than against
an exit code. `packages/spec/src/ui/view.test.ts` imports `./view.zod`
relatively, so the pins
resolve through source and no `dist` leg is involved.
| leg | mutation | predicted | observed |
|---|---|---|---|
| A | drop `.int().positive()` from the member | the refusal case
reddens, the other five hold | exactly that: `refuses a value that could
not bound a fetch — and refuses it BY NAME` failed with `gallery
limit=0: expected true to be false`; 1 failed, 5 passed |
| B | delete `limit: rowLimitKey('gallery')` | the acceptance-side cases
redden for gallery, the scope case holds | 5 failed, 1 passed —
including `expected undefined to be 100` and the parser reporting
`Unrecognized key(s) on this gallery configuration` |
| C | plant the ceiling on `GanttConfigSchema` | the scope case reddens
naming gantt | 1 failed, 5 passed: `gantt accepts an authorable row
ceiling it should not declare: expected [] to include 'limit'` |
Leg C exists because legs A and B never moved the scope pin, and a pin
never observed to fail
is not a pin. **Two things went wrong on the way there and are reported
rather than buried:**
- C's first attempt was **vacuous**: the replacement kept the anchor
text, so `ablation-replace`
refused (`the anchor count moved 1 -> 1, a drop of 0, not the declared
1`), restored, and ran
nothing. It is re-run with the anchor consumed.
- C's first real run reddened for the wrong reason: with the key
accepted there was no
`unrecognized_keys` issue to stringify, so the assertion complained
about argument types
instead of about gantt. The pin now asserts on the refused KEY LIST,
which is what makes its
red a sentence about the view type (commit `5dd3911`).
## What the change dragged with it, named rather than buried
`KanbanConfigSchema` had never carried a default, so it was on the
ADR-0122 isomorphic-pin list
(`Iso829`). An applied default gives it a second shape, which is
precisely the event that list
exists to catch, so the prescribed follow-through landed with it:
`KanbanConfigParsed` declared
beside the bare alias, the pin line removed with its own receipt, and
the pinned count plus both
prose statements of it moved 785 to 784. `check:spec-parsed-alias` is
green on the result.
That is one file outside the dispatch's declared landing surface —
`packages/spec/src/type-alias-convention.pin.test.ts` — and it is the
only one. `GalleryConfig`
and `TimelineConfig` needed nothing: both already carried defaults and
therefore both halves of
the alias pair.
The generated artifacts moved by a real (never `OS_SKIP_DTS=1`) build
and the repo's own
regenerators: `authorable-surface/ui.json` and
`authorable-defaults/ui.json` (three rows each),
`api-surface/ui.json`, `export-origins/ui.json`, the five
`api-surface-declarations/*.txt` the
view schema is embedded in, and `content/docs/references/**`. None was
hand-edited.
## Readings
All at head `5dd3911`, foreground, exit codes captured before any pipe.
- `pnpm check:adr-anchors` — exit 0: "OK (53 anchored file(s), every
governing ADR still
referenced; 133 decision number(s) ...; 36673 citation(s) across 4724
file(s) resolve)". Every
ADR id in this diff (ADR-0122, ADR-0049, ADR-0079, ADR-0087) resolves to
a real record.
- `pnpm --filter @objectstack/spec check:generated` — exit 0: "All 16
generated artifacts are up
to date."
- `pnpm --filter @objectstack/spec test` — exit 0: "Test Files 499
passed (499) / Tests 14606
passed (14606)", the six new pins among them.
- `pnpm --filter @objectstack/spec typecheck` — exit 0 (`tsc --noEmit`,
scripts project, and
`check:test-typecheck`: "54 file(s) / 259 error(s) / 144 pinned
signature(s)", the ledger
unmoved).
- `pnpm lint` — exit 0 over the whole repo (`eslint .
--no-inline-config`), so no narrowing
claim is needed.
- `node scripts/pm/dispatch-gates.mjs --ran` — "106 derived famil(ies)
accounted for — 103 run,
3 NOT-MEASURED", 0 unrun.
**NOT MEASURED, with the exit code and the reason.** Four, not the
tool's three — the fourth is
declared here because its exit code cannot say so itself:
| family | exit | why nothing was measured |
|---|---|---|
| `pnpm check:dual-build-cjs-loads` | 3 | reads built output; adapters
and apps have no `dist` in this worktree |
| `pnpm check:lean-entry-closure` | 3 | loads built entry points;
`packages/objectql/dist/core.mjs` absent |
| `pnpm check:type-check-debt` | 3 | `--re-measure` refuses: 30
workspace dependencies of the ledgered packages have no built type entry
point |
| `pnpm --filter @objectstack/spec check:skill-examples` | **1** |
"packages/client-react/dist holds no .d.ts declarations — the package is
not built". It refused after enumerating its populations and measured no
block. Its exit 1 is indistinguishable from a finding, so `--ran`
counted it among the 103 run |
All four want a repo-wide build this worktree does not carry; CI builds
everything and runs them
there. The three families whose prerequisite WAS bounded were built and
re-run rather than
declared: `@objectstack/lint`'s closure (4 packages) turned
`check:doc-formula-expressions`,
`check:doc-security-posture` and `check:docs-transcript-drift` from exit
3 into exit 0.
## Acceptance notes
- **Noted, not filed — an exit-code class worth a card, and the seat's
to file.** Six spec gates
refuse a stale `dist` with **exit 1**, the code a finding uses
(`check:api-surface`, `check:api-surface-declarations`,
`check:exported-any`,
`check:dual-source-exports`, `check:entry-nameability`,
`check:skill-examples`), while this
repo declares a distinct code for exactly that class and explains why in
`scripts/import-prerequisite.mjs`: "Exit 1 from an unmet prerequisite
and exit 1 from a real
finding are the same reading — which is why the guarded refusal below
does NOT keep that
number". The consequence is measurable and was measured here:
`dispatch-gates --ran` classifies by exit code, so it reported "103 run,
3 NOT-MEASURED" over
a record in which four families measured nothing. Successor: whoever
owns
`scripts/import-prerequisite.mjs`'s vocabulary. Dedupe words:
prerequisite, exit code, stale
dist, api-surface, dispatch-gates.
- The card's two line numbers for the declaration sites are stale
(`:945` / `:1241`); the tree
reads `:1135` and `:1437` on `0046a41b43`, as the claim comment already
corrected. Its
substantive claim — both are `strictObject`s declaring no row ceiling —
holds, and the third
site (`TimelineConfigSchema`) is declared in spec, so the card's
"timeline if spec declares a
timeline config" condition is met and timeline is in.
This PR is a draft on purpose: it owes the spec lane's at-tier contract
review, which is the
seat's to run. No labels were written from here, and the body was
written once, at creation.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9059a94 commit 1b82c51
17 files changed
Lines changed: 511 additions & 15 deletions
File tree
- .changeset
- content/docs/references
- api
- data
- ui
- packages/spec
- api-surface-declarations
- api-surface
- authorable-defaults
- authorable-surface
- export-origins
- src
- 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 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1674 | 1674 | | |
1675 | 1675 | | |
1676 | 1676 | | |
1677 | | - | |
| 1677 | + | |
1678 | 1678 | | |
1679 | 1679 | | |
1680 | 1680 | | |
| |||
1759 | 1759 | | |
1760 | 1760 | | |
1761 | 1761 | | |
1762 | | - | |
| 1762 | + | |
1763 | 1763 | | |
1764 | 1764 | | |
1765 | 1765 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
374 | 374 | | |
375 | 375 | | |
376 | 376 | | |
377 | | - | |
| 377 | + | |
378 | 378 | | |
379 | 379 | | |
380 | 380 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
828 | 828 | | |
829 | 829 | | |
830 | 830 | | |
| 831 | + | |
831 | 832 | | |
832 | 833 | | |
833 | 834 | | |
| |||
0 commit comments