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

  • 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 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 e233db9dbb9c43816b1f2c44a3381c96402334a3 → packageMentionDocs.

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 this — git 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 review、Served-tier: CONTRACT_REVIEW_TIER、Head-sha: 5dd391125ec2dc70e172cfb38da79a7a2f9994fe、①②③、Implemented-by: claude/issue-17393-view-row-ceiling、Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3、VERDICT: 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 碰 3 条 merge=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. limit 与 pagination.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.json 与 authorable-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>
os-warren pushed a commit that referenced this pull request Sep 21, 2026
…tate the gate guard the gate implements

#19228, prose + pins only. ⛔ No `.default()` moves and no precedence is
picked — both are contract directions this card is explicitly not allowed
to take.

Measured first-hand at the objectui pin `87af769e9` (2026-09-21T06:30-06:40Z),
over all 8,228 files tracked at that commit:

- `.kanban.limit` / `.gallery.limit` / `.timeline.limit` -> 0 read points,
  against 8 for the identically-shaped `.kanban.groupByField` /
  `.gallery.coverField` / `.timeline.scale` control on the same instrument.
- The row caps objectui does read are `savedViewLimit` (a view's
  `pagination.pageSize`, else its flat `limit`) and the element block's own
  flat `limit`. `ListView`'s `baseProps` carries no `limit` on any branch.
- `ElementDataSourceGate`'s arm is `!fromView || !isUsableRowLimit(authored)`,
  reading the ELEMENT-face key, which is `.optional()` with no applied
  default. The arm is reachable; the view-face default never lands on it.

So the published «fills it only when unset» was narrower than the guard, and
the per-kind key #19226 declared reaches no consumer at all. Both are now
recorded where an author and an auditor read them.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…hipped prescriptions were denying a door that exists (objectstack-ai#19234)

Fixes objectstack-ai#17487

Clause-②: no

## The defect, and its direction

Three shipped, customer-facing prescriptions in `@objectstack/spec`
stated in the present tense that the runtime confirmation door had not
shipped. It has: `actionConfirmationRefusal` is called pre-dispatch by
`invokeBusinessAction` in `@objectstack/runtime`, and the MCP
`run_action` tool grew the `confirm` member in the same change (the card
behind it, objectstack-ai#15942, is done — `state_reason=completed`; its changeset
`action-confirmation-gate-enforced` is still pending, so the door is on
`main` and not yet released).

So the published text denied a door that exists, and it failed in the
dangerous direction: an author who reads it concludes the safety flag
stops nothing, and either arranges a human in the loop some other way or
stops setting the flag — losing the gate at the moment it starts
working. That is the ADR-0049 false-compliance class with the sign
flipped.

## Re-derivation — all three sites read on today's `origin/main`

Triage's unblock comment verified site 1 only and said the other two
were unmeasured. All three were re-read at merge base `805811e0d`.

| # | Path | Current text | Verdict |
|---|---|---|---|
| 1 | `packages/spec/src/ai/tool.zod.ts` —
`TOOL_RETIRED_KEY_GUIDANCE.requiresConfirmation` | "the declaration is
the contract, not yet the behaviour — the runtime door that performs the
refusal ships separately, and until it does, setting the flag does NOT
stop an unconfirmed call. Do not try to verify the gate by invoking the
operation without the member: until that door lands, such a call simply
RUNS." | **FALSE today** |
| 2 |
`packages/spec/src/migrations/entries/semantic/17.tool-requires-confirmation-retired.ts`
— `replacement` | "The refusal is DECLARED, not yet performed — the
runtime door lands in objectstack-ai#15942, so until then the flag stops nothing on
its own and the human in the loop is still yours to arrange" | **FALSE
today** |
| 3 | the same file — `acceptanceCriteria` | "Do NOT try to 'prove the
gate' by invoking the operation without the confirmation member: the
runtime door that refuses lands in objectstack-ai#15942, so before that ships the call
is not refused, it RUNS the destructive operation." | **FALSE today** |

**Correction to the card's count of the carriers.** The card names the
`spec-changes` entry, the upgrade guide and the `os migrate meta`
projection as if they were separate sites. They are not: all three are
projections of the **one** ADR-0087 D3 entry file above. The measurement
is therefore **three false prescriptions living in two source files**,
plus three generated artefacts that carry them
(`src/migrations/registry.ts`, `spec-changes.json`,
`docs/protocol-upgrade-guide.md`), all regenerated here by
`check:generated --fix`.

Sweep radius for "is that all of them": eleven denial phrasings grepped
repo-wide (`not yet the behaviour`, `ships separately`, `not yet
performed`, `stops nothing`, `simply RUNS`, `until it does`, `until that
door`, `door lands`, `yours to arrange`, `nothing server-side`, `no
pause`), with `requiresConfirmation` lighting 10 files under
`packages/spec/src` as the positive control. Two adjacent texts were
read and left alone as **NOT A DEFECT**:
`packages/spec/src/contracts/ai-service.ts` already states the gate in
normative present tense, and `content/docs/ai/tools.mdx` says the
retired **tool**-level key "returns only together with its enforcement",
which is still true — the tool key has not returned. Two further
readings are recorded under *Acceptance notes*.

## What the prose says now, and what holds it there

Each prescription now states the refusal in the present tense **with the
door's bounds**, because an unbounded "the platform refuses unconfirmed
calls" is this same defect in the other direction. Read off the door's
own docblock and its shipped changeset, never inferred:

- the refusal is `ACTION_CONFIRMATION_REQUIRED`, 428, naming the action
and the member `confirm: true`;
- a GATE, not a queue — nothing is parked, and a refused call did not
run: the gate sits before `loadActionSubjectRecord`, so no record is
read and none written;
- the enforced set is the doors that enforce the author's `ai.exposed`
opt-in — today the action door reached from MCP `run_action`. REST
`/actions` is **not** `ai.exposed`-gated and sits outside the gate, so
an API-key agent on that route still needs its own human;
- only the author's declared `ai.requiresConfirmation: true` refuses,
and only the boolean `true` confirms; the wider `list_actions` heuristic
advises and never refuses;
- `confirm: true` is an unverifiable caller claim: the gate makes
forgetting loud, it does not prove a human.

`packages/spec/src/ai/tool-confirmation-prescription-tense.pin.test.ts`
is the tie that was missing the first time — the prose was never bound
to the function it describes, which is how it rotted. It reads the three
shipped strings **and** the runtime door, and fails in both directions.

**No pin was moved.**
`ui/action-requires-confirmation-docblock.pin.test.ts` was read: it
anchors on the `ai.requiresConfirmation` JSDoc in `ui/action.zod.ts` and
on `actionLooksDestructive`, neither of which this diff touches, so it
covers none of the three sites and stays as it is.

## Clause-②: no — the accept set did not move

`check:authorable-surface` and `check:api-surface` are green with
**zero** diff under `packages/spec/authorable-surface/` and
`packages/spec/api-surface/`. The pin's last case feeds the same
authored metadata in before and after: `tool.requiresConfirmation` still
refused, a minimal tool still accepted, `action.ai.requiresConfirmation`
still accepted for both `true` and `false`. What moved is string content
inside `dist` and `spec-changes.json`, which is why a `patch` changeset
is owed and present.

## Tests, and the reverse verification

`pnpm --filter @objectstack/spec test` — 499 files / 14614 tests passed.
`test:repo` — 34 files / 580 tests passed. `typecheck` — clean. New pin:
8/8.

Three ablation legs, each mutated on disk through
`scripts/ablation-replace.mjs` (anchor hit declared, blob hash proven to
move), direction predicted before the run, restored and proven by blob
hash against `HEAD` with `git diff HEAD` empty:

| leg | mutation | predicted | observed |
|---|---|---|---|
| 1 | re-insert `The refusal is DECLARED, not yet performed` into the D3
entry's `replacement` | RED on "no shipped prescription denies the
refusal" | RED, naming the replacement carrier |
| 2 | rename the gate call inside `invokeBusinessAction` | RED on "the
AI-facing door still calls the gate pre-dispatch" | RED |
| 3 | make the REST `/actions` door name the gate | RED on the
over-claim guard | RED |

Leg 3's **first attempt was a no-op** and is reported as such: the
replacement text still contained the anchor, so `ablation-replace`
refused (anchor drop 0, not the declared 1) and nothing ran. It was
re-anchored and re-run; the reading above is the re-run.

## Gates

All 85 commands derived by `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` for this diff were run locally
and exit 0, exit codes captured before any pipe. Eight first returned a
stale-`dist` or `PREREQUISITE NOT MET` result (exit 1 / exit 3 — not
measured, not findings); they were re-run green after `pnpm --filter
@objectstack/spec build` and a full `turbo run build` closure. `pnpm
lint` (`eslint . --no-inline-config`, whole repo, no narrowing) exits 0
at `HEAD`. CI still owns its own farm: the five path-scheduled CI jobs,
the 11 wide-population families and the artifact rosters are outside
that 85 and are NOT MEASURED here.

## Acceptance notes

Two readings taken while re-deriving, both **out of scope for this
card** and neither edited here:

1. `packages/spec/docs/MCP_GUIDE.md` (around the "Side Effects" section)
tells an author to gate side effects with "`ai.requiresConfirmation` on
the underlying **action** (+ the HITL approval queue)" and then warns,
in the adjacent block, that "nothing server-side pauses on it". The
warning is correctly scoped to the MCP capability descriptor in that
page's examples and is true of it; but the approval-queue requirement
now overstates what the action-level flag needs, and the two paragraphs
read together in the card's own dangerous direction. Not in the declared
file surface. Reported for filing with dedupe words: `MCP_GUIDE`,
`requiresConfirmation`, `HITL approval queue`, `nothing server-side
pauses`, `confirmation gate`.
2. `content/docs/ai/actions-as-tools.mdx` — the "Human-in-the-loop
approval" section still says that on the open MCP path "the approval
step lives at the protocol boundary" (client-side prompting), and the
numbered open-MCP action-gate list enumerates five gates without the
confirmation gate that now sits between the param contract and the
subject-record load. An omission against a contract that
`@objectstack/spec/contracts` declares. Reported for filing with dedupe
words: `actions-as-tools`, `human-in-the-loop`, `protocol boundary`,
`run_action`, `confirmation gate`.

Noted, not filed: `packages/spec/src/api/error-code-ledger.zod.ts` says
of the `ACTION_CONFIRMATION_REQUIRED` row that "the door will assert
this exact string by value" — a forward tense about something that is
now true. It misleads nobody about the gate and it is provenance prose
about the row's split registration, not a prescription. Successor: the
next change that touches that ledger row.

## Occupancy

Re-scanned at 2026-09-20T01:52Z over all 21 open PRs, with PR objectstack-ai#17076
(639 files) fully paged so no path is under-read.
`packages/spec/src/ai/tool.zod.ts`, the D3 entry, `spec-changes.json`,
`docs/protocol-upgrade-guide.md`, `vitest.repo-tests.json` and
`src/ai/tool.test.ts` all read FREE. Firing controls in the same scan:
`packages/spec/src/ui/component.zod.ts` HELD by objectstack-ai#19219,
`packages/spec/src/ui/view.test.ts` HELD by objectstack-ai#19226; dark control (a
nonexistent path) reads FREE. One reading to flag:
`packages/spec/src/migrations/registry.ts` reads HELD by objectstack-ai#19223, objectstack-ai#19090
and objectstack-ai#18319 — it is a generated, `merge=os-regen` artefact and none of
those three touches the D3 entry this diff edits, so the contention is
the one the regen driver exists for rather than two hands on the same
prose.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…and record where the per-kind view `limit` actually lands (objectstack-ai#19228) (objectstack-ai#19533)

Part of objectstack-ai#19228

Clause-②: no

## ⚠️ Read this first — the card's causal claim did not reproduce

objectstack-ai#19228 says an APPLIED `.default(100)` makes the react tier's 「fills it
only when unset」 arm
**structurally unreachable**. Measured end to end at the objectui pin
this repo builds against
(`.objectui-sha` = `87af769e9`, 2026-09-21T06:30–06:40Z), **it does
not**:

- The arm's guard reads the **element-face** key — `readLimit(base,
'limit')` where `base` is the
`object-kanban` / `object-timeline` node. That key is `.optional()` with
**no** applied default,
  so an author's silence is still silence at parse time.
- The applied default `objectstack-ai#19226` added sits on the **view-face** per-kind
blocks
(`GalleryConfigSchema` / `KanbanConfigSchema` / `TimelineConfigSchema`).
Different schemas.
- Nothing carries the view-face default across: `composed.limit =
config.limit ?? savedViewLimit(view)`,
and `savedViewLimit` reads `view.pagination.pageSize`, else a **flat**
`limit` on the runtime saved-view
RECORD — `ElementSavedView` is an untyped string-keyed record from the
adapter's own `listViews()`
(`core/src/data-scope/element-data-source.ts:96`, fallback at
`:237-241`), ⛔ NOT a document
`ListViewSchema` accepts: a flat `limit` on a view document is refused
`unrecognized_keys`. A third
face, named here so it is not read as the author face — and never a
per-kind block. `ListView`'s
`baseProps` (`plugin-list/src/ListView.tsx:2840-2865`) carries no
`limit` on any branch.

⛔ So no `.default()` moves in this PR, and **no precedence is picked** —
both fences the dispatch set
hold. What DID measure as a real 说明书脱节 is a different, smaller thing,
and that is what is repaired.

## What is repaired

**1. The describe's 「only when unset」 is right for its face — and now
names the face.**
The branch is `if (!fromView || !isUsableRowLimit(authored))`
(`react/src/element-data-source/ElementDataSourceGate.tsx:316-331`).
⚠️ **RETRACTED — rounds 1 and 3 both got this backwards, and round 4
measured it.** On THIS face the
displacement arm is unreachable: the key is
`z.number().int().positive().optional()` and the gate's
`isUsableRowLimit` admits integers above zero, so every value the
contract accepts is usable and the
「set but not usable」 cell is EMPTY (measured per value: 1 / 25 / 100 /
5000 accepted and usable;
0 / -1 / 2.5 / `'100'` / null refused by both). ⇒ 「fills this key only
when it is unset」 — which round 1
retired as too narrow — was never too narrow; it was right for its face,
and is restored. The view half
is `pagination.pageSize` ALONE on this face, per the bullet above. The
describe states the rule that is
reachable from the accept set; the unreachable arm is recorded in the
docblock, ⛔ not in author prose.

**2. The per-kind view `limit` is recorded as WHERE IT LANDS.** ⚠️ An
earlier revision of this section said 「read by nobody」; that was
measured with an instrument blind to spreads and is retracted — see the
CORRECTION sections below. Kanban and timeline DO read it; gallery alone
does not.
Two instruments, because the first one's answer was wrong.
PROPERTY-ACCESS over all 8,228 files tracked at the pin — probe
`\.(kanban|gallery|timeline)(\?)?\.limit\b` → 0 lines; control
`\.(kanban|gallery|timeline)(\?)?\.(groupByField|scale|coverField)\b` →
13 lines across 6 files. ⚠️ That zero is WRONG as a claim about readers:
a spread carries the key without spelling it. SPREADS, by the predicate
「a spread whose target is the object literal an adapter returns as the
node」, return four, and they overturn it. Recorded in two places, on two
different faces: on `rowLimitKey` in `view.zod.ts` (the VIEW face —
flattened onto the node and read, on both routes for kanban, on the
plugin-view route for timeline, and read by nobody for gallery), and on
the `object-timeline` door in `component.zod.ts` (the ELEMENT face — a
node's own nested `timeline.limit`, which is the one read on no route at
all). ⛔ Recorded, not repaired: what should read it is the card's open
half.

**3. `ObjectTimelinePropsSchema` carries two authorable row caps on one
strictObject.**
Taking `TimelineConfigSchema` by reference imported objectstack-ai#19226's `limit`
beside the flat `limit`. The
published member list for that door omitted the new key; it now names it
and says it is inert here.
The stale `limit` read anchor (`:234`, `:254`, at the old pin
`53ded82b`) is corrected to `:407` at
the current pin; ⛔ the other anchors in that list are NOT swept — they
remain the `53ded82b`
readings the docblock header names, and the note says so.

## The three relayed consumer readings, reproduced first-hand

Comment 5747444798 marked these 「复核的读数,⛔ 本席未复现」. All three reproduce
**by shape**; two
of the three file:line pairs are wrong at this pin, which is why shape
was the instrument.

| relayed | reproduced at pin `87af769e9` |
|:--|:--|
| `ListView.tsx:2493` forwards the timeline block nested | ✓ shape — the
nested forward is `:3084` inside `case 'timeline':` (`:3062-3117`).
`:2493` is a `useMemo` dependency array here (`:2496`). Only
`startDateField` / `endDateField` / `titleField` / `groupByField` /
`colorField` / `scale` are hoisted flat; `limit` is not |
| `ObjectTimeline.tsx:234` reads only the flat `schema.limit` | ✓ shape
— the one `$top` is `:407`, `resolveRowLimit(schema.limit,
DEFAULT_TIMELINE_LIMIT)`; also `:279` / `:281` / `:442`. No nested read
anywhere |
| nothing reads `timeline.limit` | ⚠️ RETRACTED — ✓ **0** on that
instrument, but see the CORRECTION sections: four flat spreads land the
key where kanban/timeline read it |

**The zero's instrument and its reach radius.** `git grep` over the
8,228 files tracked at commit
`87af769e9` — a literal text search of property-access spellings. Lit
control on the identical
receiver alternation (`schema.timeline` / `timelineConfig` /
`resolvedTimeline` / `mergedTimeline` /
`tCfg`): `.startDateField` → **7** hits, `.limit` → **0**. ⚠️ A first
attempt at this probe returned
0 for the control too — a broken bracket expression — and is recorded
here because a dead instrument
and a clean result are indistinguishable without one.

**One known target deliberately outside the radius:** a computed read
(`cfg[key]` with `key` from a
variable) or a wholesale spread of the config into a query builder is
invisible to a text search.
⚠️ **CORRECTION (seat, after the at-tier review at head `85ad2898`).**
That hole was named and
then closed by ASSERTION rather than by an instrument, and the assertion
was false. A spread carries
a key without ever spelling it, so this radius owed a SECOND instrument
with its own lit control; it
was not built. Built afterwards, it returns **four** flat spreads, not
one: `ListView.tsx:2979` and
`ObjectView.tsx:1638` (`...restKanban`), `ObjectView.tsx:1697`
(`...(viewOptions.gallery || {})`)
and `ObjectView.tsx:1725` (`...(viewOptions.timeline || {})`). Neither
`restKanban` destructure
strips `limit`. ⭐ Naming a limitation is not discharging it. Also
outside the radius: untracked
build output, and objectui **HEAD** rather than the pin.

## Verification

- `pnpm --filter @objectstack/spec build && typecheck && test` — **509
files / 14,890 tests pass**
  (post-merge run on this head).
- Consumer package: **`@objectstack/lint`** — the only package outside
`packages/spec` naming any of
the touched symbols (in a comment, not a runtime read; lit control:
`ListViewSchema` /
`PageComponentSchema` fire across 8+ files, so the instrument
discriminates). Its first run was a
  `Failed to resolve entry` cascade from unbuilt workspace deps; after
`pnpm --filter '@objectstack/lint^...' build` it reads **106 files /
4,034 tests pass, 5 skipped**.
- `pnpm --filter @objectstack/spec check:generated` — **all 15 generated
artifacts up to date**
(`content/docs/references/ui/component.mdx` regenerated via `--fix`;
three table rows changed in `component.mdx` and nine in `view.mdx`,
  nothing else).
- `pnpm lint` (repo-wide `eslint . --no-inline-config`) — **exit 0**,
the whole union, no narrowing.
- `check:react-declaration-parity` run exactly as `lint.yml` runs it
(`MANIFEST="$PWD/sdui.manifest.json" … --baseline
react-declaration-parity.baseline.json --strict`)
— **exit 0**, no new declaration divergence. ⚠️ Run WITHOUT the baseline
it exits 1 on 126
  pre-existing divergences; that invocation is not the gate.
- Derived gate families (`scripts/pm/dispatch-gates.mjs --commands`,
reconciled with `--ran`):
**108 derived, 100 measured green, 8 NOT MEASURED** — every one of the 8
a `PREREQUISITE NOT MET`
exit **3** (unbuilt sibling packages; `check-plugin-teardown-shape
--self-test` needs an unshallow
clone). ⛔ Reported as what they are, never as a pass. None of the 8
reads `packages/spec/src/ui/`.
- `origin/main` merged through `scripts/pm/os-regen-merge.sh`; no
regeneration debt, and main's
newest entry (`filter-between-field-reference-endpoint-refused`)
survives at the same 2-file count
  on both sides.

## ⛔ Refused / not done

- ⛔ **No precedence picked.** Which of the per-kind view `limit`, a
view's `pagination.pageSize` and
  a component's flat `limit` wins is untouched, in prose and in code.
- ⛔ **`.default(100)` not moved, demoted or removed** on any of the
three view configs.
- ⛔ **Nothing written in objectui** — read-only at the pin, as
dispatched.
- ⛔ The 14 other read anchors in the `object-timeline` docblock were NOT
re-swept at the current pin.

## Acceptance notes

- ⚠️ **File-surface deviation, declared.** The claim's declared surface
named
`packages/spec/src/ui/component.zod.ts`. Two files fall outside it:
`packages/spec/src/ui/view.zod.ts`
(the applied default the card is about lives in `rowLimitKey` **there**,
not in `component.zod.ts` —
the claim and the dispatch were both written against a stale location)
and
`content/docs/references/ui/component.mdx` (the mandatory regeneration
from a `.describe()` change;
`check:docs` reds without it — the claim anticipated regeneration but
named
`packages/spec/json-schema/`, which this diff leaves untouched). Both
are inside the seat's measured
serial-constraint clearance: zero open PRs hold any path under
`packages/spec/src/ui/`.
- ⚠️ **`Part of`, not `Fixes`.** This PR delivers the 说明书 half triage
queued; the card's own two contract questions stay open, so line 1 is
deliberately `Part of objectstack-ai#19228` and merging this does not close the card.
- noted, not filed: the `object-timeline` docblock header claims every
read anchor was taken at pin
`53ded82b` while `.objectui-sha` is `87af769e9`; only the `limit` anchor
is re-read here. Carrier: a
future `object-timeline` card, or the seat that next re-pins that block.
- noted, not filed: objectstack-ai#17393's own pin text argues an authorable ceiling
on the non-grid four 「would be
surface no renderer reads」 — the measurement above says the ceiling it
DID add is exactly that.
The pin is still correct about the four; the irony is a reading, not a
defect.

## ⚠️ CORRECTION — the per-kind view `limit` is NOT inert

Seat, after the at-tier review at head `85ad2898`, re-measured
first-hand at the pin. This PR's
earlier claim of zero read points was taken with an instrument blind to
spreads. Four flat spreads
carry the key onto the generated node (`ListView.tsx:2979`,
`ObjectView.tsx:1638` / `:1697` /
`:1725`), and `ObjectKanban.tsx` / `ObjectTimeline.tsx` read the
resulting flat `limit`.

⛔ **ADR-0049 enforce-or-remove is therefore NOT a live option for kanban
or timeline** — retiring
that key would break two adapters that actively forward it. It IS the
honest reading for **gallery
alone**, whose renderer contains no `limit` at all (0 occurrences,
against a lit control that fires
on the same file).

⚠️ One distinction this correction itself owes: the `$top` those keys
would govern is not issued on
either route today, because both hosts hand the child its rows as a
`data` prop and the child
short-circuits its own fetch. So 「governs no query on these two routes」
is true; 「read by nobody」
is not.

⭐ For the decision box: through those same spreads an APPLIED default
puts an authored-LOOKING flat
`limit: 100` on every node built from a spec-parsed view — a value no
author wrote.

## Cross-repo, ⛔ not acted on

objectui#7390's acceptance face is where this gets consumed. ⚠️
Corrected: kanban and timeline have
a read point today; **gallery** is the one that owes one. Stated here
for the seat to file in
objectui's queue with a `Blocked-by:` line; ⛔ nothing was written there.

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

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