Skip to content

feat(spec): declare the author-settable row ceiling for the page-shaped view configs - #19226

Merged
os-bill merged 6 commits into
mainfrom
claude/issue-17393-view-row-ceiling
Sep 20, 2026
Merged

os-bill merged 6 commits into
mainfrom
claude/issue-17393-view-row-ceiling

Conversation

@os-bill

@os-bill os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

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):

  • kanbanObjectKanbanSchema.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.
  • timelinelimit?: 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 strictObjects 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

…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>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 20, 2026
@github-actions

github-actions Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 9 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/api.txt, packages/spec/api-surface-declarations/data.txt, packages/spec/api-surface-declarations/root.txt, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 9 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/api.txt, packages/spec/api-surface-declarations/data.txt, packages/spec/api-surface-declarations/root.txt, …) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e233db9dbb9c43816b1f2c44a3381c96402334a3packageMentionDocs.

Which tree this was computed on

This run read content/docs from e05752f131ee0ca7f365603faf3a3b8a635c7ccd — the merge of head e1ae025756fe1b1d13c2947d9845414ef7ffafc8 into base e233db9dbb9c43816b1f2c44a3381c96402334a3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5dd391125ec2dc70e172cfb38da79a7a2f9994fe

Reviewed at head 5dd391125e (merge-base with origin/main 7e4ecc5fde is 0046a41b43; 16 files, +508 / -15). Instruments ran in a detached worktree at the head sha after pnpm install --frozen-lockfile (exit 0) and a real pnpm --filter @objectstack/spec build (exit 0, .d.ts emitted, OS_SKIP_DTS unset), so every reading below marked "ran" came from a tree where the thing could run. objectui readings are at the pinned sha 53ded82bf7 (.objectui-sha at head, unchanged by this PR), read only.

① Derived judgments

  1. Accept set — GalleryConfigSchema, KanbanConfigSchema, TimelineConfigSchema each gain limit (int, positive, default 100 APPLIED). Right. All three are strictObjects at head and exactly three limit: keys exist in view.zod.ts (:1237, :1260, :1564). Author state is limit?: number, parsed state limit: number (declared as ZodDefault over ZodNumber in api-surface-declarations/ui.txt). Refusal by name is pinned for 0, -1, 2.5, '100', null. Ran: the six spec: declare an author-settable row ceiling for gallery (and kanban) view configs — the protocol lacks the knob objectui#7390 was ruled to read (principle: 协议不正确的先改协议) #17393 pins in view.test.ts — 6 passed. TimelineConfigSchema is declared in spec (hangs off ListViewShapeSchema.timeline, :2421), so the card's timeline condition is met.

  2. Public surface — DEFAULT_VIEW_ROW_LIMIT = 100 exported from @objectstack/spec/ui; KanbanConfigParsed type exported. Right on both. api-surface/ui.json +2 rows, export-origins/ui.json +2, exported names 471 to 473.

  3. Every embedding moves with it — and only with it. Right. The 4 non-ui declaration files (api.txt, data.txt, root.txt, system.txt) add ONLY limit: number; / limit: ZodDefault… lines and header counts (awk over the added lines: zero other additions, zero removals); the symbols that gained them are GetUiViewResponseSchema, ObjectDefinitionResponseSchema, ObjectSchema, ObjectStackDefinitionSchema, ObjectStackSchema, ChangeSetSchema, CreateObjectOperation, EnvironmentArtifactSchema, MigrationOperationSchema — the view schema embedded, nothing else. The 3 .mdx diffs are the truncated kanban signature plus the new limit rows. The emitted json-schema/** (gitignored, built here) carries type: integer, exclusiveMinimum: 0, default: 100 on every embedding (ui, data Object.listViews, api, system) — measured, 80+ sites.

  4. The ADR-0122 pin removal (Iso829, 785 to 784) is the prescribed follow-through, not a silenced pin. Right, established from the tree and not from the PR's account: (a) re-adding the Iso829 line at head and running tsc --noEmit -p packages/spec/tsconfig.test.json reds with TS2344: Type 'false' does not satisfy the constraint 'true' at that line — the isomorphism genuinely stopped holding once the nested field gained .default(); (b) deleting KanbanConfigParsed from view.zod.ts reds scripts/check-spec-parsed-alias.mjs (exit 1: "KanbanConfig is the AUTHOR state of KanbanConfigSchema and nothing names its PARSED state") — the alias is load-bearing, exactly ADR-0122 D5 / D6. As landed the gate reads OK: 1451 bare z.input aliases, 784 pinned isomorphic, 667 paired. GalleryConfigParsed (:5904) and TimelineConfigParsed (:5907) pre-exist, so no line moved for them — correct.

  5. Scope — the non-grid four accept nothing. Right. GanttConfigSchema, CalendarConfigSchema, ListMapConfigSchema, TreeConfigSchema are strictObjects at head with no limit; the scope pin (which asserts on the unrecognized_keys key list, so it can only pass by naming limit as refused) passes for all four here. I did not re-run the ablation legs; I ran the pin.

  6. Two authorable row bounds on one view with no declared precedence — a real gap, judged a ③ follow-up, not a FAIL. At head the base shape's pagination.pageSize ("Number of records per page", default 25, :863) and the new per-kind limit ("sent as the query $top", default 100) are both authorable on one kanban / gallery / timeline view, and NO text in view.zod.ts says which bounds the fetch when both are written; the virtualScroll tombstone (:2591) still says "large datasets page via pagination". The PR's own rationale (point 1) named exactly this condition as disqualifying for the base-level shape, then landed it one level down — moving the placement does not remove the collision, because pagination is base-level and reachable from every kind. Measured mechanism at the objectui pin: under ListView the host fetches $top: effectivePageSize (pagination.pageSize ?? 100, ListView.tsx:989, :1966) and hands rows down as data; ObjectKanban runs its own fetch only with no bound data (ObjectKanban.tsx:287), so under the in-repo host limit never reaches a query and pagination.pageSize wins by mechanism; on a standalone authored node limit is what is read (:264). And the spec ALREADY states a precedence one face over: the react-tier ObjectKanbanPropsSchema.limit (component.zod.ts, untouched by this PR) says "the component-level dataSource.limit wins when both are set; a bound view's pagination.pageSize fills it only when unset" — with an APPLIED default the view-face limit is never unset on a parsed view, so that "fills it only when unset" arm becomes unreachable for spec-parsed views the moment a host forwards the view block (ListView.tsx:2420 spreads ...restKanban onto the node). Why not FAIL: nothing parses differently or is refused; the collision pre-exists this PR at the consumer (kanban read both keys at the pin) and in the spec's own react tier; today no behaviour depends on it (measured: hosted views never run the node fetch, standalone nodes never pass through the view parse); and closing it is one describe sentence plus a decision the card did not make and a reviewer may not make for the implementer. Why not silent: it is a contract statement the maintainer's principle (「协议不正确的应该先修改协议」) puts in the protocol BEFORE the consumer wires it — routed in ③ as a landing condition for objectui#7390's dispatch.

  7. The applied default downstream — the protocol is NOT yet the sole authority on 100; two authorities agree by luck. Measured at origin/main: the artifact door (packages/metadata/src/plugin.ts:915, ObjectStackDefinitionSchema.parse(...), then metadata = def, then registered) serves DEFAULTED views, so on that door every kanban / gallery / timeline config carries limit: 100 and objectui's $top: schema.limit ?? DEFAULT_KANBAN_LIMIT (ObjectKanban.tsx:264, constant 100 at :71; timeline :234 / :28) has a dead ?? arm — the protocol's number wins. The metadata-protocol write door keeps request.item verbatim, deliberately not parsed.data (protocol.ts:15856 ff.), and the DB loader replays conversions only (database-loader.ts:825) — on those doors limit is absent unless authored and the consumer's own 100 wins. Three declared 100s exist at head: DEFAULT_VIEW_ROW_LIMIT (applied), the react-tier describe "renderer default 100" on object-kanban.limit, and objectui's two constants. DEFAULT_VIEW_ROW_LIMIT is exported precisely so this collapses to one, but at pin 53ded82bf7 nothing reads it. Judged right as a spec change; the collapse is objectui#7390's acceptance (③).

  8. The truncation signal — declared, enforceable by no schema; acceptable ONLY because the renderer half is carded. At the pin ObjectKanban.tsx and ObjectTimeline.tsx render no truncation signal on their self-fetch path (0 hits for truncat / showing first / limitReached / role=note / footnote), and ObjectGallery.tsx neither caps nor signals; only ListView's host bar (list.dataLimitReached, :671 / :4138) signals, keyed on the host's pageSize fetch. objectui#7390 carries the signal as a hard acceptance line for the gallery (dispatch 5606587502 §四, ruling 5615809797), and the amendment 5617612958 brings kanban's and timeline's KEY under this declaration — but no line there pins a signal on kanban's and timeline's self-fetch paths. Under Prime Directive chore: version packages #10 ("file an issue") and the ruled protocol-first ordering this is acceptable; the routing gap is named in ③.

  9. The .describe() claim "sent as the query $top" — true at the pin on kanban's and timeline's self-fetch paths, not yet for the gallery (objectui#7390's leg), and not under ListView for any of the three (the host's $top is pagination.pageSize). Overstates for the hosted case; folds into the precedence sentence in ③.

② Semver level

'@objectstack/spec': minor, Clause-②: yes (widening)level right, arm right. Adds 3 authorable keys and 2 exports; removes, renames or narrows nothing; no document that parsed at the merge-base is refused at head. Gates, run here at head: scripts/pm/check-widening-tells.mjs --declaration yes --diff exit 0 (it never blocks a yes); control with --declaration no exit 4 naming 3 T1 tells (view.zod.ts:1237, :1260, :1564) and 2 T3 tells (api-surface/ui.json:110, :218) — the diff really widens, so (widening) is the correct arm and (narrowing) would be false. scripts/check-changeset-no-major.mjs --base 7e4ecc5fde --head HEAD exit 0 (no major; the level axis is PR-scoped and reads the payload in CI, where Check Changeset is green at head). scripts/check-adr-0087-registration.mjs exit 0 (non-breaking changeset, no disposition marker owed — correct, nothing is retired, so no ADR-0087 conversion). AGENTS.md rule: yes takes at least minor — met. The changeset names the one upgrade-visible consequence (a parsed config now carries limit: 100; KanbanConfigParsed is new) — an output-type gain, not a narrowing of the accept set, and the launch-window guard forbids major in any case.

③ Boundary flags

Answering report comment 5746692131 (its deviations and honest failures), plus the flags this review raises.

  • One file outside the declared surface (type-alias-convention.pin.test.ts) — ACCEPTED as required: ①4 establishes from the tree that the pin fails without the change and that the replacement is load-bearing.
  • content/docs/references/** (3 .mdx) and the 4 non-ui declaration files — ACCEPTED as generated. Verified with a firing control, not a clean git status: dirtied authorable-surface/ui.json, content/docs/references/ui/view.mdx and api-surface-declarations/ui.txt (one new row deleted from each); check:generated exit 1 naming exactly those 3 of 16 as stale; check:generated --fix regenerated exactly 3; all three back at their HEAD blobs (0510caa77205, 32acc8aaa290, 38369600f77a), git diff --stat empty. As landed, check:generated exit 0, "All 16 generated artifacts are up to date".
  • os-regen routing, for whoever lands thisgit check-attr merge at head over the 16 changed paths: 12 are merge=os-regen (the 3 .mdx, the 5 api-surface-declarations/*.txt, api-surface/ui.json, authorable-defaults/ui.json, authorable-surface/ui.json, export-origins/ui.json), 4 unspecified (the changeset, view.zod.ts, view.test.ts, the pin test). Not three. The claim comment already records authorable-surface/ui.json and api-surface-declarations/ui.txt as HELD by spec(ui): BulkActionParamSchema is strict and declares dependsOn #19090 / revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 — so this lands through scripts/pm/os-regen-merge.sh with the regeneration commit after any merge from main; the driver exits 0 while dropping a side on all 12.
  • NOT MEASURED — four families, not the tool's three — ANSWERED by CI at this head, conclusion: success: Build Core (hosts check:dual-build-cjs-loads and check:lean-entry-closure, unconditional steps, ci.yml:2057 / :2081) 01:30:18Z; Type Check · debt ledger (check:type-check-debt, lint.yml:5891) 01:31:20Z; Type Check · consumer gates (check:skill-examples, lint.yml:6268) 01:35:45Z. None of the four could plausibly hide a failure from this diff (no new import edge; no ledgered package touched; no skill example authors a parsed literal) — and CI measured it rather than reasoned it. Not measured locally by me either: they want the whole-repo build. The exit-code-class finding (six spec gates refusing a stale dist with exit 1) is a real acceptance note and the seat's to file; not this PR's.
  • Ablation leg C — vacuous first attempt, wrong-reason red on the second — ACCEPTED as honest reporting. The landed pin asserts on the refused key list, so it can only pass by an unrecognized_keys issue naming limit, and it reds naming the view type when the key is accepted. I did not re-run the legs; I ran the pins (6 of 6) and the two source controls in ①4.
  • Commit trailers — VERIFIED: all four commits carry Claude-Session: plus Co-authored-by: Claude and no model identifier; AGENTS.md wins over the harness reminder.
  • CI not awaited at report time — awaited here: at 01:46Z six of the seven required contexts are success at head (Build Core, TypeScript Type Check, Test Core, Dogfood Regression Gate, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard); Lint & Repo Gates is still in_progress. The one gate in that job a new schema key could plausibly move, check:react-declaration-parity --baseline react-declaration-parity.baseline.json --strict, ran here as CI spells it: exit 0, "no new DECLARATION divergence vs accepted baseline"; component.zod.ts references none of the three view config schemas, so this diff cannot reach it. Console Pin Gate is skipped by path filter and the PR removes nothing the pinned sibling imports. Landing still waits on that seventh context per Multi-agent discipline §7, regardless of this record.
  • Reading discrepancy, not a defect — the report cites ObjectKanbanSchema.limit in @object-ui/types (objectql.zod.ts:1762); that is a reading at objectui HEAD dda8f3815d, not at the pin 53ded82bf7, where objectql.zod.ts declares no kanban limit at all and the key is read only at ObjectKanban.tsx:264. The substantive claim (consumer-local key, read as $top, constant 100) holds at both trees; cite the pin.
  • FOLLOW-UP 1 (spec, before objectui#7390 dispatches) — declare precedence. One card, domain:spec: the per-kind limit describe (and pagination's) must state what wins when both are written, reconciled with the react-tier sentence already in component.zod.ts ("limit wins; pagination.pageSize fills it only when unset"), and say what an APPLIED default does to that arm. If the seat wants pagination.pageSize to keep filling an unset ceiling, the fix is a described-not-applied default (drop .default(), keep the describe, re-add Iso829, drop KanbanConfigParsed); if limit is meant to win unconditionally, write that. Protocol first — this is a prerequisite of the objectui#7390 dispatch, not of this merge.
  • FOLLOW-UP 2 (objectui#7390 acceptance) — one authority on the number, and the signal on all three kinds. Replace DEFAULT_KANBAN_LIMIT (ObjectKanban.tsx:71) and DEFAULT_TIMELINE_LIMIT (ObjectTimeline.tsx:28) with DEFAULT_VIEW_ROW_LIMIT from @objectstack/spec/ui, read the spec-declared key on all three kinds, and pin the visible truncation signal on kanban's and timeline's self-fetch paths as well as the gallery's — otherwise two of the three kinds land declared-but-unhonoured at release. The react-tier "renderer default 100" describe on object-kanban.limit should cite DEFAULT_VIEW_ROW_LIMIT, in the same spec card as FOLLOW-UP 1.

Implemented-by: claude/issue-17393-view-row-ceiling
Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3

VERDICT: PASS


Generated by Claude Code

os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Provenance — at-tier contract review adopted, carriers cleared, landing three checks passed

domain:spec seat 2 执行席(session_01JbZnqu8bt6YqfJsr9vaFb3),2026-09-20T01:54Z,与剥标、ready、arm 同一动作。

Record of record: 本 PR 评论 5746847791 —— ## Contract reviewServed-tier: CONTRACT_REVIEW_TIERHead-sha: 5dd391125ec2dc70e172cfb38da79a7a2f9994fe、①②③、Implemented-by: claude/issue-17393-view-row-ceilingReviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3VERDICT: PASS(带两条具名后续)。⛔ 逐字采纳,未改写未润色。

档位证据,读自子代理自己的转录(逐请求 harness model,常量从 scripts/pm/dispatch-gates.mjs 现场 import):

CONTRACT_REVIEW_TIER (imported live) = claude-fable-5-1
transcript lines=286  type:"assistant" lines=136  unparseable=0
   136  claude-fable-5-1   ✅ AT TIER

get_session 未采信,⛔ 子代理自述档位不作证据。

三条落地前检:

# 读数
达档条款②复核 PASS 在案 ✅ 5746847791,形状七项全真,零尖括号
双载体已清,机读 ✅ PR 与卡 #17393 同笔剥标;check-clause2-carriers --pair 19226(仪器取自 origin/main)→ exit 0
全绿,⛔ 非必查子集 ✅ 39 个 check:34 绿、5 skip、0 红、0 未完;mergeable_state: clean

⚠️ 一个数我自己先报错了,当场更正并重测

本席早先按施工报告说本 PR 碰 3merge=os-regen 路径。复核实测是 12,本席用 git check-attr merge 逐文件独立重跑,确认 12:

content/docs/references/{api/protocol,data/object,ui/view}.mdx
packages/spec/api-surface-declarations/{api,data,root,system,ui}.txt
packages/spec/api-surface/ui.json · authorable-defaults/ui.json
packages/spec/authorable-surface/ui.json · export-origins/ui.json
[firing control] content/docs/references/ui/view.mdx     -> os-regen
[dark control]   packages/spec/src/ui/view.zod.ts        -> unspecified   ← 真·被跟踪文件,非虚构路径

⇒ 教训记下:⛔ 生成物路由不能按报告里的类别名估,只能逐文件问 check-attr

os-regen 落地前测量(⛔ 这在本 PR 上不是形式)

merge=os-regen 的驱动会 exit 0、零冲突标记地静默丢掉一侧,所以入队前测的是「主干在合并基之后动过这些路径没有」:

merge-base = 0046a41b43        origin/main = 7e4ecc5fde
12 条 os-regen 路径中,自合并基以来在主干上变动过的:  0
[firing control] 同区间主干上确实变动过的文件总数:      6
[dark control]   一条不可能变动的路径:                  0

⇒ 亮对照证明这把尺子在响,所以那个 0 是读数。驱动此刻无可丢。 落地后本席会对这 12 条再做一次 head↔origin/main 逐字节复核。

串行接力:本轮只放行这一张

12 条路径里 11 条被其它开放 PR 占着(#19024 / #19090 / #19130 / #19147 / #19219 / #18319 / #19223)。按 「多个已实现 PR 全碰生成物 ⇒ 串行接力一次只放行一个」,本席此刻队列里没有自己的 PR(#19215 已落地,#19223 在补丁轮),因此放行这一张即满足接力。⛔ 冲突解法恒为整体重生成,⛔ 永不文本合并。

复核留下的两条后续 —— 归本席跟,⛔ 不随本 PR 落地消失

  1. limitpagination.pageSize 的优先级没有裁决。 react 层(component.zod.ts,本 PR 未碰)已经写着 object-kanban.limit 的一条优先级句;视图面加了已应用的默认值之后,那句话的一条分支对已解析视图不可达。复核判这是 ③ 后续而非 FAIL,理由写在记录里(今天无行为依赖、冲突在消费端与 react 层先于本 PR 存在)。本席据此立卡。
  2. objectui#7390 的验收面:采用 DEFAULT_VIEW_ROW_LIMIT(退掉 DEFAULT_KANBAN_LIMIT / DEFAULT_TIMELINE_LIMIT)、三种视图都读该键、并把可见截断信号钉在 kanban 与 timeline 的自取数路径上。

并附一条读数归属更正:施工报告称 @object-ui/types 声明了 kanban 的 limit(objectql.zod.ts:1762)。复核在 pin 53ded82bf7 上读到的是没有——那个 :1762 是 objectui HEAD dda8f3815d 的读数。⇒ 「消费端已经在读」这句话在 pin 上不成立,在 HEAD 上成立。这不改变本 PR 的判决(spec 侧声明该键本就是先手),但它是 objectui#7390 验收时必须带着的事实。

落地路径:ready → auto-merge → 合并队列。⛔ 队列外不合并。


Generated by Claude Code

os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

队列处置审计 —— 被踢出的原因是生成物冲突,⛔ 不是红,⛔ 不是 flaky

domain:spec seat 2(session_01JbZnqu8bt6YqfJsr9vaFb3),2026-09-20T02:28Z。⛔ 本条不重投,重投前先修。

读数

added_to_merge_queue      @ 01:55:26Z
removed_from_merge_queue  @ 01:57:27Z        ← 入队后 2 分 01 秒
mergeable_state           = dirty
git ls-remote origin 'refs/heads/gh-readonly-queue/*'   → 本 PR 不在其中
git log --oneline origin/main | grep -F '(#19226)'      → 无
  [firing control] grep -F '(#19215)' → 1      [dark control] grep -F '(#99999999)' → 0

⇒ 两分零一秒远不够跑完 merge group 的构建(Lint & Repo Gates 在那里实测 26–29 分钟,见 #19189),而 dirty合并冲突的读数。⇒ 不是被红踢的,是变得不可合并。⛔ 没有 merge-queue triage 评论,这与 triage 只在 failure 时留言一致,⛔ 不作「无人诊断」读。

冲突源,测到而非推断

#19219 在本 PR 入队后落到 main(e233db9dbb)。与本 PR 16 条路径求交:

main 在 7e4ecc5fde..e233db9dbb 之间动了 19 个文件
OVERLAP  packages/spec/api-surface-declarations/ui.txt
OVERLAP  packages/spec/api-surface/ui.json
OVERLAP  packages/spec/authorable-surface/ui.json
OVERLAP  packages/spec/export-origins/ui.json
  ==> 4 条,全部是生成物,全部 merge=os-regen
[dark control] 与一条不可能相交的路径求交 → 0

这正是本 PR provenance 里预告过的那个条件在兑现:那条评论写了 12 条 os-regen 路径中 11 条被其它开放 PR 占着,#19219 就在名单上。⇒ 入队前的测量没有错(彼时主干在合并基之后动过 0 条 os-regen 路径,亮对照 6、暗对照 0);⛔ 错的是把那个 0 当成了会保持。生成物占用是一个会在你排队时改变的读数。

处置

不重投(「一次处置」的额度未动用:重投只用于已知 flaky、或死在测试体之前、或同 commit 早前绿过 —— 三条都不成立)。按车道纪律,碰生成物的 PR 走固定四步序:

bash scripts/pm/os-regen-merge.sh      # 在该分支的 worktree 内

⛔ 永不 rebase、⛔ 永不 force-push、⛔ 永不手工编辑生成物。该脚本头部自陈两个顺序陷阱:os-regen 路径的合并会 exit 0、零冲突标记地静默丢一侧;在仍处 merge 状态时跑 gen:schema 会把 authorable-surface 锚点悄悄回滚到老分叉点,而回滚后的锚点仍然「真」,于是门禁全过、一次已落地的推进被抹掉。指令已发给施工席。

这张 PR 落地前还欠一次复核 —— 因为 head 会动

达档复核记录 5746847791(PASS)钉在 head 5dd391125e。同步+重生成会移动 head ⇒ 按 「head 后移或无结论才重挂」,本 PR 将重新挂 needs:contract-review 并欠一次新的达档复核。⛔ 这不是对上一轮复核的否定:它的 ①②③ 全部站住,PASS 只带了两条已立卡的后续(#19228、objectui#7390 验收面)。⛔ 在新记录到位前,本席不翻 ready、不入队、不 arm。


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>

os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e1ae025756fe1b1d13c2947d9845414ef7ffafc8

Second at-tier record on a NEW head. The first record (comment 5746847791, head 5dd391125e, PASS with two named follow-ups) is not re-rolled here; the head moved because the queue ejected the PR on a generated-artifact conflict with #19219 and a sync plus whole regeneration followed (5c753f997a merge of e233db9dbb, then e1ae025756). The live question is what the sync changed, dropped or smuggled — and whether this head is worse than the one that passed. Every "ran" reading below came from a detached worktree at e1ae025756 after pnpm install --frozen-lockfile (exit 0) and a real pnpm --filter @objectstack/spec build (exit 0, OS_SKIP_DTS unset, 46 .d.ts emitted), so the things measured could actually run there; git-only readings name their shas. Instruments (check-widening-tells, check-changeset-no-major, check-adr-0087-registration, check-spec-parsed-alias, check-generated, check-governed-merges) are byte-identical between this head and origin/main 847e5773a4 (blob compare per script), so the head-tree copy IS the origin/main copy. objectui readings are at the pin 53ded82bf7 (.objectui-sha unchanged at both heads and on main), read only.

① Derived judgments

  1. The implementation bytes did not move. git rev-parse per file, reviewed head vs this head: view.zod.ts blob 65853f52f3 at both, view.test.ts 191ec8e982 at both, type-alias-convention.pin.test.ts 276d3fc346 at both, .changeset/17393-view-row-ceiling.md 675b2c05b0 at both. Every ① judgment of record 5746847791 about those four files therefore still describes this head verbatim, and I re-ran their pins rather than re-reading them: the six spec: declare an author-settable row ceiling for gallery (and kanban) view configs — the protocol lacks the knob objectui#7390 was ruled to read (principle: 协议不正确的先改协议) #17393 pins in view.test.ts — 6 passed (vitest, project local, source import); the ADR-0122 pin file — 3 passed at 784; pnpm check:spec-parsed-alias — OK, "1452 bare z.input aliases, 784 pinned isomorphic, 668 paired" (667 to 668 is feat(spec): declare element-level navigation on object-kanban / object-calendar and give object-timeline its ComponentPropsMap row (#17987) #19219's own pair arriving with the merge, not this PR's).

  2. Both sides survive in the generated artefacts — measured at line grain and at the right path grain, with controls. The four merge=os-regen paths both branches moved are api-surface/ui.json, api-surface-declarations/ui.txt, authorable-surface/ui.json, export-origins/ui.json. The merge commit 5c753f997a holds the BRANCH side of all four (blob equals 5dd391125e's in each), i.e. the driver exited 0 and silently dropped feat(spec): declare element-level navigation on object-kanban / object-calendar and give object-timeline its ComponentPropsMap row (#17987) #19219's side exactly as the implementer reported; e1ae025756 is the commit that discharges it. At e1ae025756: every line main added in 7e4ecc5fde..e233db9dbb (3 / 286 / 17 / 3 per file) is present, and every line this PR added in 0046a41b43..5dd391125e (2 / 32 / 3 / 2) is present — the only absences on either side are the two superseded header-count lines of ui.txt, whose head values are the sum of both sides on both axes (exported names 471 base plus 3 main plus 2 PR equals 476; declarations 485 plus 3 plus 2 equals 490); every line either side removed is absent. By quoted name with PATHS (git grep -c at the head sha): ui/GalleryConfig:limit, ui/KanbanConfig:limit, ui/TimelineConfig:limit each 1 in packages/spec/authorable-surface/ui.json AND 1 in packages/spec/authorable-defaults/ui.json — and 0 under api-surface-declarations/, which is scope, not loss: those keys never lived there. DEFAULT_VIEW_ROW_LIMIT and KanbanConfigParsed: api-surface-declarations/ui.txt 2 each, api-surface/ui.json 1 each, export-origins/ui.json 1 each. feat(spec): declare element-level navigation on object-kanban / object-calendar and give object-timeline its ComponentPropsMap row (#17987) #19219's side: ui/ObjectKanbanProps:navigation (authorable-surface/ui.json:806), ui/ObjectCalendarProps:navigation (:699), ObjectTimelinePropsSchema (ui.txt 4, api-surface/ui.json 1, export-origins/ui.json 1, component.mdx 1). Dark controls ui/GalleryConfig:zzNoSuchKey__ and ui/ObjectKanbanProps:zzNoSuchKey__: 0 paths. Sibling implementation body intact: of the 19 paths main moved in 7e4ecc5fde..e233db9dbb, 14 are blob-identical at head (including component.zod.ts d663b3c630, component-element-navigation-17987.test.ts, its changeset, declaration-map/ui.json, json-schema.manifest/ui.json, dropped-refinements.baseline.json) and the other 5 are the generated ones re-derived below.

  3. The sync smuggled nothing; one path appeared and it is a joint product, reproduced by the generator. Three-dot contribution at the reviewed head (0046a41b43..5dd391125e): 16 paths, +508 / -15. At this head (e233db9dbb...e1ae025756): 17 paths, +511 / -15. Per-file added-and-removed line multisets compared between the two: 15 of 17 identical. The two that differ: (a) content/docs/references/ui/component.mdx appeared, +1 line — the limit row under ### Nested Shape: ObjectTimelineProps.timeline (:831). Cause, established from the tree: feat(spec): declare element-level navigation on object-kanban / object-calendar and give object-timeline its ComponentPropsMap row (#17987) #19219 gave ObjectTimelinePropsSchema its ComponentPropsMap row and that schema embeds TimelineConfigSchema as its timeline member (component.zod.ts:3932); component.zod.ts referenced TimelineConfigSchema 0 times at 0046a41b43 and 5dd391125e, 5 times at e233db9dbb and head. This PR adds limit to TimelineConfigSchema, so on the merged tree gen:docs renders it under the timeline props page. Neither parent alone produces that line; the merged tree does, and the generator reproduces it byte-exact (④). (b) api-surface-declarations/ui.txt 34 to 36 contribution lines: two extra limit: ZodDefault over ZodNumber declaration lines — the same embedding, inside ObjectTimelinePropsSchema's declaration — plus the header counts above. Nothing else entered or left the diff; no hand-written path outside the PR's four; no .gitattributes, workflow, script or baseline moved.

  4. The committed artefacts are exactly what the tooling produces, proven with a firing control, not a clean status. Clean tree after the real build: pnpm --filter @objectstack/spec check:generated exit 0, "All 16 generated artifacts are up to date". Firing control: deleted one new row from each of authorable-surface/ui.json (ui/TimelineConfig:limit), component.mdx (:831) and api-surface-declarations/ui.txt (first DEFAULT_VIEW_ROW_LIMIT line); check:generated exit 1 naming exactly check:authorable-surface, check:api-surface-declarations, check:docs — "3 of 16 artifact(s) stale"; --fix regenerated exactly 3; all three back at their HEAD blobs (32cf71ec88, 4c71915193, ec6a2389bb), git status --porcelain empty. Then the WHOLE chain, not --fix: gen:export-origins, gen:schema, gen:api-surface, gen:api-surface-declarations, gen:docs in the runbook's order — 13 of 13 os-regen paths at their HEAD blobs afterwards, 0 mismatches, git status --porcelain empty across the whole tree. So the regeneration commit is the generator's output on the merged sources, on every path including the one that appeared.

  5. os-regen routing at this head — 13 of 17, per file. git check-attr merge in the head worktree (its .gitattributes blob 56d9f9bcbb equals origin/main's) over every changed path: merge=os-regen on 13 — content/docs/references/api/protocol.mdx, data/object.mdx, ui/component.mdx, ui/view.mdx; packages/spec/api-surface-declarations/api.txt, data.txt, root.txt, system.txt, ui.txt; packages/spec/api-surface/ui.json; authorable-defaults/ui.json; authorable-surface/ui.json; export-origins/ui.json. unspecified on 4 — the changeset, view.zod.ts, view.test.ts, type-alias-convention.pin.test.ts. Firing control packages/spec/spec-changes.json (tracked, not in this PR) reads os-regen; dark control packages/spec/src/ui/component.zod.ts reads unspecified. The first record's 12 was right for its 16-path diff; the delta is component.mdx from ③.

  6. Governed register — no hit. GOVERNED_SURFACES in scripts/pm/check-governed-merges.mjs at 847e5773a4 is docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, docs/NORTH-STAR.md; 0 of the 17 paths match, and AGENTS.md fed to the same predicate matches (firing control). CI Governed Surface Queue Guard success at this head 02:45:23Z.

  7. What the merged tree now declares that neither parent did — the one respect in which this head is worse, and why it is a follow-up amendment rather than a FAIL. At this head ObjectTimelinePropsSchema (component.zod.ts:3924) carries BOTH a flat limit: z.number().int().positive().optional() describing "renderer default 100" (:3945) AND timeline: TimelineConfigSchema.optional() (:3932), whose limit now APPLIES 100. OBJECT_TIMELINE_FLAT_CONFIG_KEYS (:3851) does not list limit, so the flat spelling is not guided into the block: one strict node accepts two spellings of one concept, one optional and one always present after parse, with no text saying which wins, and the generated component page shows both rows (component.mdx:809 and :831). Measured at the pin: ListView's timeline branch forwards the block NESTED (timeline: resolvedTimeline, ListView.tsx:2493) and lifts only groupByField / colorField / scale and the date fields flat (:2523 to :2525); ObjectTimeline.tsx reads only the flat schema.limit (:234, $top: schema.limit ?? DEFAULT_TIMELINE_LIMIT, constant 100 at :28); nothing at the pin reads timeline.limit (git grep 0). So the PR body's point (3) — "the per-kind block is what actually reaches the renderer" — holds for kanban (spread flat) and does NOT hold for timeline at the pin, where the applied default lands nested and unread. Why not FAIL, by the first record's own standard (its ①6): nothing parses differently and nothing that parsed before is refused; the nested key reaches no query today, so no behaviour depends on it; the renderer half is carded (objectui#7390) and the precedence question is carded (spec(ui): the new per-kind view limit and the base pagination.pageSize are two authorable row bounds with no declared precedence — and an APPLIED default makes the react tier's own "fills it only when unset" arm unreachable #19228); and the shape was created by the merge of two PRs each PASS-reviewed on its own, so it belongs to the successor card, not to either. Why not silent: it is a third instance of spec(ui): the new per-kind view limit and the base pagination.pageSize are two authorable row bounds with no declared precedence — and an APPLIED default makes the react tier's own "fills it only when unset" arm unreachable #19228's question, now inside a single strict object, and the acceptance face of objectui#7390 must read timeline.limit (or the protocol must say the flat key is the timeline's carrier) — stated in ③.

  8. Landing state, measured. Main moved one commit past the PR base (847e5773a4, docs(qa): re-point the cli / platform-core / records-forms bad-citation anchors (#18104 cut 3) #19232, 4 docs/qa paths, overlap with the PR's 17 = 0). Driver-free probe (git clone --bare --shared, no merge.os-regen.driver registered, merge-tree --write-tree --name-only 847e5773a4 e1ae025756): exit 0, tree 7cc6d3fa2b, no conflicted path — GitHub's mergeable_state reads clean. That is a reading at 847e577, and the provenance comment's own lesson stands: generated occupancy changes while a PR is queued, so it is the seat's to re-measure at arm time.

② Semver level

'@objectstack/spec': minor, Clause-②: yes (widening)level right, arm right, re-adjudicated on this head's larger diff. scripts/pm/check-widening-tells.mjs --declaration yes --diff over the 2314-line three-dot diff: exit 0. Control --declaration no: exit 4 naming exactly the same five tells as the first record — T1 view.zod.ts:1237, :1260, :1564; T3 api-surface/ui.json:110, :218 — so the merge added no tell of its own (the timeline-props embedding is a consequence of an existing tell, not a new key or export). scripts/check-changeset-no-major.mjs --base e233db9dbb --head HEAD: exit 0, no major; the level axis is PR-scoped and CI Check Changeset is success at head. scripts/check-adr-0087-registration.mjs --base e233db9dbb --head HEAD: exit 0, "adds no declared-breaking changeset (1 non-breaking changeset(s) seen)" — nothing retired, no disposition owed. Adds 3 authorable keys and 2 exports, removes, renames or narrows nothing; AGENTS.md's "yes takes at least minor" is met. The changeset bytes are unchanged, so its migration-facing prose (a parsed config now carries limit: 100; KanbanConfigParsed is new) still matches the tree.

③ Boundary flags

What 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 limit and pagination.pageSize, now card #19228) and FOLLOW-UP 2 (objectui#7390's acceptance face: one authority on the number via DEFAULT_VIEW_ROW_LIMIT, the key read on all three kinds, the truncation signal pinned on kanban's and timeline's self-fetch paths) are carded and are not re-litigated here. Its reading-attribution correction stands too: the view.zod.ts docblock still cites objectql.zod.ts:1762 at objectui dda8f3815d, which is a HEAD reading and not a pin reading; a nit, unchanged. Its "NOT MEASURED — four families" item is answered again by CI at this head: Build Core 02:53:16Z, Type Check · debt ledger 02:53:00Z, Type Check · consumer gates 02:53:15Z, all success; the exit-code class is now card #19227.

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: object-timeline declares limit (flat, react tier, optional, described 100) and timeline.limit (view face, applied 100) on one strict node with no precedence text, and at the pin the host forwards the block nested while the renderer reads the flat key (①7). The card as filed names the kanban react-tier sentence and the view face; it should also name this node, and objectui#7390's acceptance face should say which of the two the timeline honours. Nothing at this head changes FOLLOW-UP 2's content; it sharpens its timeline leg.

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 5dd391125e nor e233db9dbb declared, and the PR body's rationale (3) is now demonstrably kanban-only at the pin. In every other respect measured, no: the implementation bytes are unmoved, both sides' rows survive at line grain, the artefacts are byte-exact generator output with a firing control, the only new path is the generator's own product of the two changes, the tells are the same five, no governed or hand-written path entered the diff, and all seven required contexts are success at this head (Lint & Repo Gates 03:11:34Z, TypeScript Type Check 02:57:39Z, Test Core 03:03:15Z, Dogfood Regression Gate 02:56:19Z, Build Core 02:53:16Z, Temporal Conformance (live PG + MySQL) 02:56:08Z, Governed Surface Queue Guard 02:45:23Z). The "worse" item is contract prose owed to a successor card, not a parse, a refusal or a runtime path, so it does not move the verdict.

Other flags, answering report comment 5747309707.

  • The runbook was followed and the drop was real — ACCEPTED, from the tree rather than the report: 5c753f997a holds the branch side of all four both-sides paths (the driver's silent drop of feat(spec): declare element-level navigation on object-kanban / object-calendar and give object-timeline its ComponentPropsMap row (#17987) #19219's rows), and e1ae025756 restores them by regeneration (①2, ①4). 5c753f997a is the runbook's git merge --no-edit auto-message with no trailer; scripts/check-commit-card-trailers.mjs pins that exact shape as clean in its self-test, and e1ae025756 carries the model-free pair. I could not inspect $GIT_DIR/os-regen-pending or the pre-commit refusal the report describes (local hook state in the implementer's worktree); the outcome it claims — a single regeneration commit whose staged diff restores the sibling's rows — is what the tree shows.
  • check:react-declaration-parity — the first record reasoned this gate unreachable because component.zod.ts referenced no view config schema; at this head it references TimelineConfigSchema, so the reasoning no longer holds and the gate was RUN as lint.yml:6344 spells it (MANIFEST="$PWD/sdui.manifest.json" … --baseline react-declaration-parity.baseline.json --strict): exit 0, "no new DECLARATION divergence vs accepted baseline"; object-timeline reads 2 declared by both, 13 spec-only, 0 registry-only, all pre-accepted by feat(spec): declare element-level navigation on object-kanban / object-calendar and give object-timeline its ComponentPropsMap row (#17987) #19219's baseline row.
  • The docs-drift bot comment (5746686202) computed on the merge ref with "uncommitted changes" — that is the bot's own checkout state, not this PR's; the head tree is clean after a whole-chain regeneration (①4).
  • Not measured here: the full pnpm --filter @objectstack/spec test (500 files) and pnpm lint — taken from CI (Test Core and Lint & Repo Gates success at head) and from the report; I ran the two test files this PR touches and the three ADR-0122 / generated-artifact gates directly. The ablation legs were not re-run; the bytes they falsify are unmoved, and the pins were.

Implemented-by: claude/issue-17393-view-row-ceiling
Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3

VERDICT: PASS


Generated by Claude Code

os-bill commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Provenance(第二次)—— 同步后的达档复核已采纳,三条落地前检通过

domain:spec seat 2 执行席(session_01JbZnqu8bt6YqfJsr9vaFb3),2026-09-20T03:48Z,与剥标、ready、arm 同一动作。

Record of record: 本 PR 评论 5747419039,Head-sha: e1ae025756fe1b1d13c2947d9845414ef7ffafc8,VERDICT: PASS。⛔ 逐字采纳。前一份记录 5746847791(head 5dd391125e,亦为 PASS)因 head 后移已不指向要落地的东西,按 「head 后移或无结论才重挂」 重挂重审,本条是新记录的 provenance。

档位证据(读自子代理自己的转录,常量现场 import):

CONTRACT_REVIEW_TIER (imported live) = claude-fable-5-1
transcript lines=243  type:"assistant" lines=100  unparseable=0
   100  claude-fable-5-1   ✅ AT TIER

三条落地前检:

# 读数
达档条款②复核 PASS 在案 ✅ 5747419039,形状七项全真,零尖括号
双载体已清,机读 ✅ PR 与卡 #17393 同笔剥标;--pair 19226(仪器取自 origin/main)→ exit 0
全绿,⛔ 非必查子集 ✅ 39 个 check:34 绿、5 skip、0 红、0 未完

⭐ 这一轮真正被验的是「静默丢弃」有没有被修回来

这张 PR 上一次被踢出队列是生成物冲突(审计见 5746759712 所在那条)。同步用的是四步序,而 os-regen 驱动确实丢了一侧:复核逐行核过,合并提交 5c753f997a 保留的是分支侧,#19219 的行被丢;第 4 步的整体重生成把它们还回来了。复核在 head 上做了行级双向断言:main 加的每一行(3/286/17/3)与本 PR 加的每一行(2/32/3/2)都在,删的都不在;暗对照 0。

⚠️ 本席自己第一次验这件事时验错了量程,记在这里:我把探针限定在 packages/spec/api-surface-declarations/ 里查,六个里五个读 0 —— 那五个 0 是量程错误不是丢失,那些键住在 authorable-surface/ui.jsonauthorable-defaults/ui.json。去掉路径限制并打印路径之后两侧都在。⇒ 「对照必须与判断同量级;当搜索范围本身是待判之物时,数个数不是读数」。

入队前的 os-regen 漂移,在 arm 这一刻重测

⚠️ 上一轮的教训是:这个 0 不会自己保持——当时测得 0,排队两分钟后 #19219 落地就把它变成了冲突。所以这次在 arm 的同一动作里重测:

merge-base = e233db9dbb        origin/main = 9059a9472a
自合并基以来在主干上变动过的 os-regen 路径:  0
[firing control] 同区间主干变动过的文件总数:  12
[dark control]                               0

亮对照在响 ⇒ 那个 0 是读数。os-regen 路由本身也重测过:13 / 17(4 个 mdx + 5 个 declarations + 4 个 ui.json),⛔ 不按类别名估。

串行接力:本席此刻队列里没有其它 PR(#19223 在同步中,#19235 在复核中),放行这一张即满足「一次只放行一个」。

⭐ 复核指出的「这个 head 比通过的那个更差」的一处 —— ③,非 FAIL

合并后的树在同一个 strict 节点 object-timeline 上同时声明了 react 层的扁平 limit(optional,「renderer default 100」)与视图面的 timeline.limit(已应用 100),两个父提交都没有这个组合,且无优先级文本。复核在 pin 53ded82bf7 上实测:ListView 把 timeline 块嵌套转发,而 ObjectTimeline.tsx:234 只读扁平的 schema.limit,没有任何东西读 timeline.limit。⇒ PR 正文理由 (3) 在 pin 上只对 kanban 成立

判为 #19228 的第三个实例而非 FAIL(无解析差异、无行为依赖、两半都已立卡)。本席据此去补 #19228,并把「timeline 兑现哪个键」写进 objectui#7390 的验收面。

落地路径:ready → auto-merge → 合并队列。⛔ 队列外不合并。


Generated by Claude Code

@os-bill
os-bill marked this pull request as ready for review September 20, 2026 03:48
@os-bill
os-bill added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 1b82c51 Sep 20, 2026
44 checks passed
@os-bill
os-bill deleted the claude/issue-17393-view-row-ceiling branch September 20, 2026 04:18
os-bill pushed a commit that referenced this pull request Sep 20, 2026
`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>
os-bill pushed a commit that referenced this pull request Sep 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

2 participants