feat(spec): declare the author-settable row ceiling for the page-shaped view configs - #19226
Conversation
…d view configs `GalleryConfigSchema`, `KanbanConfigSchema` and `TimelineConfigSchema` each gain a `limit` member: an int-positive row ceiling with the default applied (100), whose describe states that default and the visible truncation signal the renderer owes when the ceiling applies. `DEFAULT_VIEW_ROW_LIMIT` is exported so a consumer reads the number instead of re-declaring it. The name and the placement are the protocol absorbing keys the consumers already read: objectui caps kanban (`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`) and timeline off keys declared in `@object-ui/types` and on a component props interface, never in the protocol. The non-grid four keep their platform ceiling and gain nothing here. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
The applied default on `KanbanConfigSchema.limit` gives that schema a second shape, which is the event `type-alias-convention.pin.test.ts` exists to catch: its `Iso829` pin leaves, `KanbanConfigParsed` is declared beside the bare alias as ADR-0122 prescribes, and the pin count plus both prose statements of it move 785 -> 784. Also carries the regenerated authorable-surface and authorable-defaults rows the three new keys add. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…the changeset api-surface, api-surface-declarations, export-origins and the docs references tree, regenerated with the repo's own tooling after a real (non-OS_SKIP_DTS) build. The four non-ui declaration files move only where the view schema is embedded in them. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…ringified issue Measured during the ablation that falsifies it: with a ceiling planted on the gantt config there is no `unrecognized_keys` issue to stringify, so the case reddened with an argument-type complaint instead of a sentence about gantt. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 136 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 e05752f131ee0ca7f365603faf3a3b8a635c7ccd && git checkout e05752f131ee0ca7f365603faf3a3b8a635c7ccd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e233db9dbb9c43816b1f2c44a3381c96402334a3 e1ae025756fe1b1d13c2947d9845414ef7ffafc8 && git checkout -B drift-repro e233db9dbb9c43816b1f2c44a3381c96402334a3 && git merge --no-ff e1ae025756fe1b1d13c2947d9845414ef7ffafc8
node scripts/docs-audit/affected-docs.mjs --json e233db9dbb9c43816b1f2c44a3381c96402334a3 |
Contract reviewServed-tier: Reviewed at head ① Derived judgments
② Semver level
③ Boundary flagsAnswering report comment 5746692131 (its deviations and honest failures), plus the flags this review raises.
Implemented-by: VERDICT: PASS Generated by Claude Code |
Provenance — at-tier contract review adopted, carriers cleared, landing three checks passed
Record of record: 本 PR 评论 5746847791 —— 档位证据,读自子代理自己的转录(逐请求 harness ⛔ 三条落地前检:
|
队列处置审计 —— 被踢出的原因是生成物冲突,⛔ 不是红,⛔ 不是 flaky
读数⇒ 两分零一秒远不够跑完 merge group 的构建( 冲突源,测到而非推断
⭐ 这正是本 PR provenance 里预告过的那个条件在兑现:那条评论写了 12 条 os-regen 路径中 11 条被其它开放 PR 占着,#19219 就在名单上。⇒ 入队前的测量没有错(彼时主干在合并基之后动过 0 条 os-regen 路径,亮对照 6、暗对照 0);⛔ 错的是把那个 0 当成了会保持。生成物占用是一个会在你排队时改变的读数。 处置⛔ 不重投(「一次处置」的额度未动用:重投只用于已知 flaky、或死在测试体之前、或同 commit 早前绿过 —— 三条都不成立)。按车道纪律,碰生成物的 PR 走固定四步序: ⛔ 永不 rebase、⛔ 永不 force-push、⛔ 永不手工编辑生成物。该脚本头部自陈两个顺序陷阱:os-regen 路径的合并会 exit 0、零冲突标记地静默丢一侧;在仍处 merge 状态时跑 这张 PR 落地前还欠一次复核 —— 因为 head 会动达档复核记录 5746847791(PASS)钉在 head Generated by Claude Code |
Sync with origin/main through scripts/pm/os-regen-merge.sh, whose step 2 restored main's side of the four os-regen artifacts both branches moved (api-surface/ui.json, api-surface-declarations/ui.txt, authorable-surface/ui.json, export-origins/ui.json) — the driver had merged them exit 0 while silently keeping one side. Regenerated from the merged tree after a real build, so both sides' entries are present: #19219's element-level `navigation` rows and its ObjectTimelineProps block, and this branch's three `limit` rows plus DEFAULT_VIEW_ROW_LIMIT and KanbanConfigParsed. gen:docs adds the timeline row to the component reference page. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Second at-tier record on a NEW head. The first record (comment 5746847791, head ① Derived judgments
② Semver level
③ Boundary flagsWhat stands from record 5746847791, cited by id. All nine of its ① judgments stand — the files they judge are byte-identical at this head and their pins were re-run green here; its ② stands with the gates re-run above on the larger diff; its FOLLOW-UP 1 (precedence between the per-kind What this head changes for the follow-ups — one amendment, the seat's act, not the implementer's. #19228 gains a third instance at this head: Is this head worse than the head that passed? In one respect, yes — ①7: the merged tree declares a two-spelling row ceiling on a single strict node that neither Other flags, answering report comment 5747309707.
Implemented-by: VERDICT: PASS Generated by Claude Code |
Provenance(第二次)—— 同步后的达档复核已采纳,三条落地前检通过
Record of record: 本 PR 评论 5747419039, 档位证据(读自子代理自己的转录,常量现场 import): 三条落地前检:
⭐ 这一轮真正被验的是「静默丢弃」有没有被修回来这张 PR 上一次被踢出队列是生成物冲突(审计见 5746759712 所在那条)。同步用的是四步序,而 os-regen 驱动确实丢了一侧:复核逐行核过,合并提交
入队前的 os-regen 漂移,在 arm 这一刻重测亮对照在响 ⇒ 那个 0 是读数。os-regen 路由本身也重测过:13 / 17(4 个 mdx + 5 个 declarations + 4 个 ui.json),⛔ 不按类别名估。 串行接力:本席此刻队列里没有其它 PR(#19223 在同步中,#19235 在复核中),放行这一张即满足「一次只放行一个」。 ⭐ 复核指出的「这个 head 比通过的那个更差」的一处 —— ③,非 FAIL合并后的树在同一个 strict 节点 判为 #19228 的第三个实例而非 FAIL(无解析差异、无行为依赖、两半都已立卡)。本席据此去补 #19228,并把「timeline 兑现哪个键」写进 objectui#7390 的验收面。 落地路径:ready → auto-merge → 合并队列。⛔ 队列外不合并。 Generated by Claude Code |
`main` moved under this branch: #19226 landed the author-settable row ceiling and touched the same two generated artifacts this branch owns. Both are routed to the `os-regen` merge driver, which merges them with exit 0 while silently keeping one side, so `scripts/pm/os-regen-merge.sh` was run: it merged `origin/main`, took main's side of the two artifacts in the worktree, and committed the merge first. This is its step 4 — regenerate on the committed merge, never a text merge and never a hand edit. Regenerated with the repo's own tooling on a REAL build (no `OS_SKIP_DTS`): `build` -> `gen:schema` -> `gen:api-surface-declarations` -> `gen:docs`. `check:api-surface-declarations` reads "declaration text unchanged (17 entry points, 5364 declarations)" and `check:generated` is green on all 16 artefacts. Both sides asserted present afterwards, against the STAGED index blobs as well as the worktree, with a dark control reading 0: #19226's `ui/GalleryConfig:limit` / `ui/KanbanConfig:limit` / `ui/TimelineConfig:limit`, `DEFAULT_VIEW_ROW_LIMIT` and `KanbanConfigParsed`; and this branch's own `RecordRelatedListProps.columns` union with its nested `columns[number]` docs table. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…ged tree Baseline drift, not a code change — the fourth sync lap on this branch. Main's #19226 and #19235 moved `packages/spec/api-surface-declarations/{data,root, system,ui}.txt`, and that directory is a `merge=os-regen` path, so the merge produced four files current for neither side. Regenerated from a real build of the merged tree (34/34 declaration files emitted; ⛔ no `OS_SKIP_DTS`), via `scripts/pm/os-regen-merge.sh`, with `MERGE_HEAD` confirmed absent first — the build opens with `gen:schema`, and running that in MERGE state is the anchor-rollback trap.⚠️ The `MM` grade was live here and was read on purpose. After regenerating, the index held main's side (803/535) while the worktree held the regeneration (323/4); a bare `git commit` would have landed the index. `git add -A` first, then `git diff --cached` re-read as the 323/4 it should be, and every one of the four index blobs hash-matches its worktree file. BOTH SIDES asserted by quoted-exact name over the WHOLE TREE with paths printed, then again against the index blobs, with a dark control reading 0 files: this branch's facade signature and prescription constant; #19226's `DEFAULT_VIEW_ROW_LIMIT`, `KanbanConfigParsed` and the three `ui/{Gallery,Kanban,Timeline}Config:limit` keys; #19235's `RecordRelatedListProps.columns[number]` and `z.array(ListColumnSchema)`; and #19219's `ObjectTimelinePropsSchema` carried forward. Note the three `limit` keys live ONLY in `authorable-surface/ui.json` and `authorable-defaults/ui.json` and the related-list row ONLY in `content/docs/references/ui/component.mdx` — a grep scoped to the declaration files reads 0 for them out of range, not loss. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #17393
Clause-②: yes (widening)
GalleryConfigSchema,KanbanConfigSchemaandTimelineConfigSchemaeach gain alimitmember —
z.number().int().positive().default(100)— andDEFAULT_VIEW_ROW_LIMITis exportedbeside 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
limiton the three page-shaped config blocks. Rejected: a member on thebase view config (
ListViewShapeSchema). Three properties of this tree decide it, not taste:pagination.pageSize(PaginationConfigSchema, default 25). A second base-level row keywould leave one view with two base-level row bounds and no declared precedence between them,
and the
virtualScrolltombstone at the bottom of that same shape already prescribespaginationfor exactly that question ("large datasets page viapagination").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.
dda8f3815d:ListView's kanban branch destructures the merged block and spreads the restflat onto the generated
object-kanbannode (packages/plugin-list/src/ListView.tsx, the...restKanbanin that branch's return), so a protocolkanban.limitlands exactly whereObjectKanban.tsx:573already readsschema.limit. A base-level key is forwarded into noper-kind node at all.
The NAME follows the same evidence:
limitis the name the consumers already read, so thisdeclaration 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 schemadoes 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
$topwithout 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 repoin no way):
ObjectKanbanSchema.limit, declared in@object-ui/typesalone(
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:573as$top: schema.limit ?? DEFAULT_KANBAN_LIMIT,with that constant
= 100at:84.limit?: numberonObjectTimeline's own props interface(
packages/plugin-timeline/src/ObjectTimeline.tsx:129) and on no published schema at all;read at
:328as$top: schema.limit ?? DEFAULT_TIMELINE_LIMIT, that constant= 100at:29.$topand zerolimitinpackages/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_LIMITwith. 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 (anchorcount, replacement count, blob hash) and proves the restore against
HEADrather than againstan exit code.
packages/spec/src/ui/view.test.tsimports./view.zodrelatively, so the pinsresolve through source and no
distleg is involved..int().positive()from the memberrefuses a value that could not bound a fetch — and refuses it BY NAMEfailed withgallery limit=0: expected true to be false; 1 failed, 5 passedlimit: rowLimitKey('gallery')expected undefined to be 100and the parser reportingUnrecognized key(s) on this gallery configurationGanttConfigSchemagantt 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:
ablation-replacerefused (
the anchor count moved 1 -> 1, a drop of 0, not the declared 1), restored, and rannothing. It is re-run with the anchor consumed.
unrecognized_keysissue to stringify, so the assertion complained about argument typesinstead 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
KanbanConfigSchemahad 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 listexists to catch, so the prescribed follow-through landed with it:
KanbanConfigParseddeclaredbeside 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-aliasis 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.GalleryConfigand
TimelineConfigneeded nothing: both already carried defaults and therefore both halves ofthe alias pair.
The generated artifacts moved by a real (never
OS_SKIP_DTS=1) build and the repo's ownregenerators:
authorable-surface/ui.jsonandauthorable-defaults/ui.json(three rows each),api-surface/ui.json,export-origins/ui.json, the fiveapi-surface-declarations/*.txttheview 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 stillreferenced; 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 upto date."
pnpm --filter @objectstack/spec test— exit 0: "Test Files 499 passed (499) / Tests 14606passed (14606)", the six new pins among them.
pnpm --filter @objectstack/spec typecheck— exit 0 (tsc --noEmit, scripts project, andcheck:test-typecheck: "54 file(s) / 259 error(s) / 144 pinned signature(s)", the ledgerunmoved).
pnpm lint— exit 0 over the whole repo (eslint . --no-inline-config), so no narrowingclaim 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:
pnpm check:dual-build-cjs-loadsdistin this worktreepnpm check:lean-entry-closurepackages/objectql/dist/core.mjsabsentpnpm check:type-check-debt--re-measurerefuses: 30 workspace dependencies of the ledgered packages have no built type entry pointpnpm --filter @objectstack/spec check:skill-examples--rancounted it among the 103 runAll 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) turnedcheck:doc-formula-expressions,check:doc-security-postureandcheck:docs-transcript-driftfrom exit 3 into exit 0.Acceptance notes
refuse a stale
distwith 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 thisrepo 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 realfinding 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 --ranclassifies by exit code, so it reported "103 run, 3 NOT-MEASURED" overa record in which four families measured nothing. Successor: whoever owns
scripts/import-prerequisite.mjs's vocabulary. Dedupe words: prerequisite, exit code, staledist, api-surface, dispatch-gates.
:945/:1241); the treereads
:1135and:1437on0046a41b43, as the claim comment already corrected. Itssubstantive claim — both are
strictObjects declaring no row ceiling — holds, and the thirdsite (
TimelineConfigSchema) is declared in spec, so the card's "timeline if spec declares atimeline 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