Skip to content

feat(rest,console): the record page payload serves the 动态 count, so the activity part asks only the panel's rows - #28991

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-6jkgb1
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-6jkgb1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #28737

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — no file under skills/** teaches the record page payload's parts; skills/objectstack-ui/rules/record-page.md teaches the page's layout, which this does not change. The hand-written content/docs/frontend/plugins/plugin-detail.mdx Console Integration section is updated.

维护者速读

  • 改了什么:记录页请求 POST /data/:object/:id/page 新增可选部件 timeline,由服务端统计这条记录完整 动态 流的条数(评论 + 动态 + 字段变更,去掉面板不画的类型,有字段变更时 updated 动态让位)。控制台用它显示 动态 计数徽标、「查看全部 N 条」和 动态 标签页计数,动态 部件只请求面板要画的 5 行,不再一次取 200 行。
  • 为什么改:feat(rest,console): a record open is one request; the page payload carries the record's streams and the related rows' names #28682 里动态部件为了让计数正确只能取 200 行,这是它的合约审查 should-fix ③-2 记下的偏差。现在计数由服务端给出,行数可以按面板来取。
  • 风险与回滚:协议只增不减(Clause-② 加宽,Metadata-protocol: added,@objectstack/spec 与 @objectstack/rest 定为 minor),新成员全部可选,旧客户端不受影响。RECORD_PAGE_MAX_ACTIVITY_ROWS(200)和 RecordPageActivityRequestSchema 的上下限都没动。评论或动态读取被拒时整个 timeline 部件被拒,字段变更读取被拒时记 0 条。回滚就是还原本 PR。
  • 先看:packages/spec/src/api/record-page.zod.ts(timeline 的来源表和共享的类型排除常量),packages/rest/src/record-page-routes.ts 的 answerTimeline,ui/packages/app-shell/src/views/RecordDetailView.tsx(什么时候只请求 5 行、什么时候读完整窗口)。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/app-shell/src/views/RecordDetailView.timelineCount-28737.test.tsx,四条通过:超过 200 条的记录显示服务端给的 341,请求只要 5 行;没有 动态 的记录显示 0,也没有回退读取;直接打开 动态 标签页时请求 200 行;去掉服务端计数的消融测试会退回本地计数 6。
  • 需要你做的:总监做合约审查(CONTRACT_REVIEW_TIER),由席位转交。PR 保持草稿。

What changes

The record page payload counts the record's 动态 stream on the server, so the console no longer reads 200 activity rows to name a count. The 动态 panel draws its newest 3 or 5 entries, and the page now asks only those rows.

Contract members added (one complete list for the contract review)

@objectstack/spec (packages/spec/src/api/record-page.zod.ts, exported from @objectstack/spec/api):

Request (RecordPageRequestSchema, still .strict()):

  • timeline?: { discussion: boolean; activity: boolean; fieldChanges: boolean } (RecordPageTimelineRequestSchema, strict, every source required): ask for the count of the 动态 stream drawn from the sources named.

Response (RecordPageResponseSchema):

  • timeline?: { total: int ≥ 0 } | { error } (RecordPageTimelineCountSchema | RecordPageStreamRefusalSchema).

Constants:

  • RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES = ['commented', 'mentioned', 'login', 'logout']: the sys_activity types the stream draws no entry for (satisfies readonly SysActivityBuiltinType[]).
  • RECORD_TIMELINE_UPDATE_ACTIVITY_TYPE = 'updated': the activity type that steps aside once field changes are in the stream.

New exports: those two schemas and two constants, and for each schema its X (input) and XParsed (output) type. RECORD_PAGE_MAX_ACTIVITY_ROWS stays 200 and RecordPageActivityRequestSchema keeps its bounds (ruling: not narrowed here).

@object-ui/types (ui/packages/types/src/data.ts): RecordPagePayload.timeline?: { total: number } | { error: RecordPagePartRefusal }. The request type is the spec's RecordPageRequest, so it gains timeline with no separate declaration.

@object-ui/plugin-detail: exports RecordTimelineTotalContext (the host's count of the whole stream, number | null), RECORD_TIMELINE_PANEL_MAX_ENTRIES (5, the wide panel's rows) and RECORD_PAGE_TIMELINE_PANEL.

What the count is

Source Counted as the caller's read When named
discussion sys_comment where thread_id = {object}:{id} ($count, $top=0) the panel draws the discussion
activity sys_activity for the record, type not in RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES the panel draws activity
fieldChanges sys_audit_log for the record, action in create, update, delete field history is on for the object

The total is their sum, less the activity updated rows when field changes are counted and there is at least one (the stream's own mergeTimelineStream rule). Each read goes through findData as the caller, behind the stream object's API exposure gate, like the parts #28682 added. A refused comment or activity read refuses the part. A refused sys_audit_log read counts 0, because the console draws no field change it cannot read. A source the boot does not register (OBJECT_NOT_FOUND) holds no entry.

One definition for the dropped types (ruling: name which)

One shared definition. RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES lives in the spec. The server's count excludes it ($nin), and the console's activity reading (ui/packages/plugin-detail/src/renderers/recordActivityFeed.ts) builds its "no entry" rows of ACTIVITY_TYPE_TO_FEED_TYPE from the same constant. mergeTimelineStream uses RECORD_TIMELINE_UPDATE_ACTIVITY_TYPE. A test pins it as well: the undrawn keys of ACTIVITY_TYPE_TO_FEED_TYPE equal the constant exactly (RecordTimelineTotal-28737.test.tsx), and the spec test pins the constants as builtin activity types.

Console

  • The page asks timeline whenever it draws the 动态 count, which is the synthesized columns page, with the sources the panel draws. activity asks RECORD_TIMELINE_PANEL_MAX_ENTRIES (5) rows, or the 200-row window when the page opens on its 动态 tab (?related=timeline).
  • The count chip, 「查看全部 N 条」, the 动态 tab's count and the tab row's probe name the served total (RecordTimelineTotalContext). They still name nothing while the feed loads or when a read is refused. Without a served total (an authored page, a refused part, a data source without getRecordPage) each counts the rows it holds, as before.
  • Comments written on the page after it opened stay counted: the total swaps the payload's comment count for the comments the page holds now.
  • Opening the 动态 tab later reads the 200-row activity window once (sys_activity, $top=200, $expand=actor_id), and only when the payload asked the panel's rows and the record holds more activity rows than that. A record with none makes no extra read.

Docs

content/docs/frontend/plugins/plugin-detail.mdx, Console Integration: the request snippet now asks activity: { top: 5 } and timeline, and the prose says when the tab reads the window. The reference page content/docs/references/api/record-page.mdx and the other spec artifacts are regenerated.

Tests

  • packages/spec/src/api/record-page.test.ts [#28737]: the request member is strict (every source required, booleans only, no extra key); the response is a count or a refusal; the constants are builtin activity types and leave updated out.
  • packages/rest/src/record-page-routes.test.ts [#28737] (8): the sum and the exact reads asked; only the named sources are read; zero; a refused comment or activity read refuses the part; a refused ledger read counts 0; not asked means not served; the exposure gate refuses without reading; the body stays strict.
  • packages/qa/dogfood/test/record-page-timeline.dogfood.test.ts (3, real HTTP + security stack, plugin-audit's real writers, org-bound boot): the served total equals the sum of the separate $count list reads asked as the same member, with the updates stepping aside; without field changes the updates stay; only the named sources are counted. Armed controls: the stores hold the stream (3 comments, ≥ 3 ledger rows, 2 updates) and the member reads each source.
  • ui/packages/plugin-detail/src/__tests__/RecordTimelineTotal-28737.test.tsx: the chip and 「查看全部」 show the served 341 while 5 rows are held; a served 0 shows 0; no count while loading or refused; control without a served count shows the held 6; the full stream and the probe report the served count; the shared-definition pin; the update steps aside.
  • ui/packages/app-shell/src/views/RecordDetailView.timelineCount-28737.test.tsx (the card's two tests and more): a record with more than 200 merged entries shows the served 341, the page request asks activity: { top: 5 } and timeline, and opening the 动态 tab reads the window once; a record with none shows 0 and after opening the tab only the page request went out; opening on ?related=timeline asks 200 rows and makes no activity read; ablation without the served count shows the held 6.
  • Updated expectations, same behaviour otherwise: RecordDetailView.recordOpenOneRequest-28470, recordOpenWaves-13010 and recordPagePayload-27627 now expect activity: { top: 5 } plus timeline on the synthesized page.

Acceptance notes

  • Ruled sources only. The count is comments + drawn activity + ledger writes, less the updates. Known places where the drawn stream can still differ from the count, all toward the count being the larger: the console draws a ledger row only when it has a change it can display (collectAuditChanges), and it reads the newest 50 ledger rows. The count names every ledger write the caller can read.
  • The tab pages through what it holds. The 动态 tab can page through the 200 activity rows and 50 ledger rows it holds while the count names the whole stream, as it did before this change for a record with more than 200 rows.
  • A refused ledger read counts 0 by design: the console draws no field change it cannot read. Pinned in the rest unit test only, because on the dogfood boot the platform's default sets let a member read the ledger rows of records they read.
  • Serial constraint. Seat 1's console phone record page: two bars stacked (app bar + record top bar) and the app bar repeats the record name — where do the app menu, search, notifications and account go? #28483 (RecordDetailView guard states) is already on main, and this branch has merged it (clean merge, re-verified at the new head), so this PR lands second.
  • Size. 1366 changed lines (1281 + / 85 −), under 3000, so not Tier H.

Four axes

  • 实际业务需求:动态 面板只画 3 或 5 条,但每次打开记录都要取 200 行动态来算计数,这是 feat(rest,console): a record open is one request; the page payload carries the record's streams and the related rows' names #28682 合约审查记下的实测偏差(should-fix ③-2)。打开任何有 动态 的记录都会走这条路径。
  • 项目长远合理性:计数是服务端对同一个流的读,契约先在 spec 里声明(来源表、拒绝规则、共享常量),实现兑现声明。没有在消费端截断或猜计数,也没有收窄已发布的 RecordPageActivityRequestSchema。
  • 防 AI 写错:被丢弃的类型只有一个定义,服务端和控制台都从 spec 常量取,另有测试钉住两边完全一致,以后加一个不画的类型不会只改一边。请求体保持 strict,三个来源都必须明确给出布尔值,拼错或漏写会得到 400。
  • 创业阶段不扩散:只加一个可选的计数部件,消费者就是记录页本身。没有新门禁,没有分页或按类型细分的计数,RECORD_PAGE_MAX_ACTIVITY_ROWS 不变。

Verification

At the head after merging main:

  • @objectstack/spec record-page.test.ts 10/10; @objectstack/rest record-page-routes.test.ts 28/28; rest typecheck (incl. check:test-typecheck) green; spec check:test-typecheck green.
  • dogfood record-page-timeline.dogfood.test.ts 3/3.
  • ui plugin-detail, whole package: 361 files passed, 1 skipped; 3197 tests passed.
  • ui app-shell RecordDetailView* + the console phone record page: two bars stacked (app bar + record top bar) and the app bar repeats the record name — where do the app menu, search, notifications and account go? #28483 header test + the new plugin-detail test after the merge: 98 files, 610 tests passed. Before the merge, the 151 other ui test files that import RecordDetailView, recordPagePayload or getRecordPage passed (1397 tests).
  • turbo run type-check for @object-ui/types, @object-ui/plugin-detail, @object-ui/app-shell: 67/67.
  • pnpm --filter @objectstack/spec check:generated: all 14 artifacts current, against a dist this run built.
  • node scripts/check-sdui-manifest.mjs: intact.
  • node scripts/pm/dispatch-gates.mjs --commands sweep: see the os-dev-report on the card.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QsR2jSrQVdYaC3GSLbQCFd

Generated by Claude Code

…ks only the panel's activity rows

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSrQVdYaC3GSLbQCFd
…ine part; the record-open tests and the plugin-detail docs ask the panel's rows

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSrQVdYaC3GSLbQCFd
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/spec, touching 18 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 00d0668d380046198dfc9151858b88df463bfd9b.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 171 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 00d0668d380046198dfc9151858b88df463bfd9b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 81f65b36f6ecca3c12f079295cacf6d0cf6b443c — the merge of head af2b6b3f339361b216bac0fefbb17330f9c41f4d into base 00d0668d380046198dfc9151858b88df463bfd9b, 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 81f65b36f6ecca3c12f079295cacf6d0cf6b443c && git checkout 81f65b36f6ecca3c12f079295cacf6d0cf6b443c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 00d0668d380046198dfc9151858b88df463bfd9b af2b6b3f339361b216bac0fefbb17330f9c41f4d && git checkout -B drift-repro 00d0668d380046198dfc9151858b88df463bfd9b && git merge --no-ff af2b6b3f339361b216bac0fefbb17330f9c41f4d

node scripts/docs-audit/affected-docs.mjs --json 00d0668d380046198dfc9151858b88df463bfd9b

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 00d0668d380046198dfc9151858b88df463bfd9b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator

director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw

Contract-review-claim: session_01QsR2jV5sBSzCYNEDgpYLJw · head af2b6b3 · 2026-11-02T06:02Z

Face: schema (packages/spec/src/api/record-page.zod.ts), served at CONTRACT_REVIEW_TIER, routed by domain:ui#4. Read-only; the record follows as its own comment on this PR.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: af2b6b3f339361b216bac0fefbb17330f9c41f4d
Local-runs: none
Scope: packages/spec/src/api/record-page.zod.ts + packages/rest/src/record-page-routes.ts (answerTimeline, readStream), packages/spec/src/data/feed.zod.ts (SYS_ACTIVITY_BUILTIN_TYPES), ui/packages/plugin-detail/src/renderers/recordActivityFeed.ts (ACTIVITY_TYPE_TO_FEED_TYPE, applyFeedConfig), ui/packages/plugin-detail/src/recordTimelineStream.ts (mergeTimelineStream, auditEntriesToFeedItems), ui/packages/app-shell/src/views/RecordDetailView.tsx (the page's timeline plan, its sys_comment and sys_audit_log reads), packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts (action values), packages/qa/dogfood/test/record-page-timeline.dogfood.test.ts

Inputs: card #28737 (body and its 4 comments, the os-dev-report 6570130917 included), PR #28991 body and file list, the net diff against main at the head, and the head's check-runs as of 2026-11-02T05:58Z: 27 completed success, 7 skipped, 33 still in progress (Test Core 8 shards, Test UI 16 shards, Type Check ×3, Build ×3, Lint UI, Live E2E UI, Build & E2E UI), 0 failed. The green ones that bear on this face: Check Changeset, Spec property liveness, Lint · repo & tooling gates, Lint · PM dispatch-gates, Governed Surface Queue Guard, Dogfood Regression Gate, PR-body guards. The lane seat lands only on the fully green head, as the queue guard requires.

① Derived judgments

  • RecordPageRequestSchema gains timeline?: RecordPageTimelineRequestSchema (strict, three required booleans). An optional member on a strict object widens the accept set and refuses nothing a main client sends: right. The strictness (a timeline: true, a missing source, an extra key are 400) is pinned in the spec test and the route test: right.
  • RecordPageResponseSchema gains timeline?: RecordPageTimelineCountSchema | RecordPageStreamRefusalSchema ({ total: int ≥ 0 } strict, or { error }). Optional, same refusal shape as the other stream parts (Record page open: the discussion, the activity feed, the attachment count and the related rows' reference names arrive in the record's page payload, so one open is one request plus the shell's own reads #28470): right. { total, error } and { total, records } refused by the test: right.
  • Four new exports from @objectstack/spec/api: the two schemas with their X/XParsed types, RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES (as const satisfies readonly SysActivityBuiltinType[]) and RECORD_TIMELINE_UPDATE_ACTIVITY_TYPE. Additive; the type-only import from ../data/feed.zod follows the existing api→data imports (analytics.zod.ts, batch.zod.ts, contract.zod.ts): right. The test pins every undrawn type as a built-in and updated as not undrawn: right.
  • RECORD_PAGE_MAX_ACTIVITY_ROWS stays 200 and RecordPageActivityRequestSchema keeps 1..200: no narrowing, as the card's scope note and the claim ruled: right. The docblock's wording moves the count's ownership to the timeline part: right.
  • Five regenerated spec artifacts (api-surface, authorable-surface, declaration-map, export-origins, json-schema.manifest) carry the four exports; Spec property liveness and the repo gates are green on the head: right.
  • The first consumer (answerTimeline) reads each source with findData as the caller, behind the stream object's exposure gate via readStream, with limit: 0, count: true. That is the route's own related-list pattern (limit: entry.top, count: true, top min 0) and total falls back to records.length only when the driver answers no total: right. $nin is a declared filter operator (analytics.zod.ts, field.test.ts): right.
  • OBJECT_NOT_FOUND on a source counts 0 (an unregistered sys_audit_log on a boot without plugin-audit) while a refused comment or activity read refuses the part and a refused ledger read counts 0. That is the console's own asymmetry: RecordDetailView catches and warns on a failed sys_audit_log read and draws no field change, but surfaces commentsDenied/activityDenied: right, and the spec prose says it.
  • FIELD_CHANGE_ACTIONS = ['create','update','delete'] against sys_audit_log.action's twelve values (read, login, import, action_invocation, … among them): the writes are the rows that carry a before/after the stream can draw: right.
  • @object-ui/types RecordPagePayload.timeline? optional: right. No ui/.changeset entry, as feat(rest,console): a record open is one request; the page payload carries the record's streams and the related rows' names #28682 (16 ui files) landed without one and Check Changeset is green: right.

② Semver level

.changeset/28737-record-page-timeline-count.md: @objectstack/spec minor, @objectstack/rest minor; Clause-②: yes (widening); Metadata-protocol: added. The diff publishes exactly a widening (new optional request and response members, new exports, a new served part on POST /api/v1/data/{object}/{id}/page) and no narrowing. The PR body carries the same two lines. Check Changeset and check-changeset-no-major green on the head. Matches.

③ Boundary flags

④ Design judgment

packages/spec/src/api/record-page.zod.ts, judged against card #28737's Wanted 1 ("the server's own count of the same merged stream the panel draws, with the panel's type exclusions") and its first consumer (answerTimeline, then the console's chip, 「查看全部 N 条」, the 动态 tab count and the tab-row probe through RecordTimelineTotalContext).

The contract holds. The timeline table defines the count by rows, source by source, with the one subtraction mergeTimelineStream makes (the updated activity rows when any field change is counted), and the route computes exactly that table; the dogfood suite proves it equal to the separate $count reads as the same member on a real boot with plugin-audit's writers. The "one definition" for the dropped types is real: ACTIVITY_TYPE_TO_FEED_TYPE now builds its undefined rows from the spec constant and mergeTimelineStream uses the update constant, so a type added to one side cannot be missed on the other. I tried the breaks the card invites: a record with more than 200 activity rows (the count no longer saturates); a reply (counted on both sides: the tab draws replies as items, the peek names roots only but reports all.items.length); a source the boot does not register (0, as the page reads an empty stream); the comment thread written to after open (the console swaps the payload's comment count for the comments it holds); a refused ledger read (0 on both sides). None breaks.

  • should-fix ④-1 (prose in the hits file, no schema change). The docblock at lines 70–77 ("The one definition … Every other value … is an entry") and the table at 156–176 read as if the count equals the drawn stream. Two console rules the table does not carry make the drawn stream smaller than the count, never larger: (a) applyFeedConfig hides a completed activity (task kind) unless the panel sets showCompleted, the row is a milestone or a logged interaction; the server cannot tell those rows apart without reading each row's metadata, so it counts every completed row; (b) auditEntriesToFeedItems draws a ledger write only when collectAuditChanges finds a displayable diff, and the console reads the newest 50. The PR body's Acceptance notes name (b) and not (a). Write both into the RecordPageTimelineRequestSchema docblock (one sentence after "The total is their sum."): the count is the sources' rows as this table defines them; the panel's own display configuration (showCompleted, types) and a ledger row with nothing displayable draw fewer, so the count is an upper bound of the drawn entries and equal to them on the default panel without completed tasks. A reader of the spec then knows what total is and is not, which is the point of declaring it first. Not a must-fix: the contract as written is exact, testable and pinned, the card's own scenarios hold, and the consumer's chip was already computed over a different row set than the tab draws before this PR.
  • Read, not a finding: when both activity and fieldChanges are named the updates count is read in parallel even when changes turns out 0 and it is not used. One read per page open on a record without field changes; sequencing it would add a round trip to the common case, so the parallel read is the right trade.

Implemented-by: claude/project-thread-6jkgb1
Reviewed-by: session_01QsR2jV5sBSzCYNEDgpYLJw

VERDICT: PASS

Should-fix ④-1 is prose in the schema file; if the seat takes it on this branch before landing, the head moves on a prose-only hunk and carries under Regen-provenance:/prose carry rules; if it lands as is, the seat files it as a follow-up with this comment as the reference.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

席位核对 · ACCEPT · 2026-11-02T06:16Z

Head-sha: af2b6b3f339361b216bac0fefbb17330f9c41f4d

  • Shape: draft → main, Fixes #28737 in the body only. 28 files, +1281 / −85 (1366 lines ≤ 3000). No governed path: check-governed-merges --pr 28991 hit 0 of 28 paths. Author objectstack-fleet[bot]. Clause-②: yes (widening), Metadata-protocol: added and the Skills-docs: none — … third line are line-initial in the body, and the changeset carries both declaration lines.
  • Contract review: the schema face (packages/spec/src/api/record-page.zod.ts) has a CONTRACT_REVIEW_TIER PASS from the director seat, record 6570231752 on this head. Its one should-fix, ④-1, is prose only: the docblock should say the count is an upper bound of the drawn entries. The seat filed it as record-page timeline count: the docblock should say the total is an upper bound of the entries the 动态 panel draws #28998 so the reviewed head does not move. This ACCEPT is the lane seat's TIER_DEFAULT read of the rest of the diff.
  • Read in the diff (rest, ui, types):
    1. The rest route counts each named source with readStream(name, { limit: 0, count: true }), so each count goes through that object's exposure gate as the caller's read. A refused comment or activity count refuses the part. A refused ledger count counts as zero field changes, and an unregistered source counts as zero. Its metadata wave reads each stream object once (new Set).
    2. The undrawn activity types are one definition. ACTIVITY_TYPE_TO_FEED_TYPE now derives its undrawn rows from RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES, and mergeTimelineStream steps aside on RECORD_TIMELINE_UPDATE_ACTIVITY_TYPE. The client and the server cannot drift.
    3. The console asks for the count only when the page draws the 动态 panel. It asks for the activity top 5 (the panel's most) unless the page opens on ?related=timeline, where it keeps the 200 window. The tab reads the 200 window once on open, and only when total says more rows exist.
    4. RECORD_PAGE_MAX_ACTIVITY_ROWS and the request bounds are unchanged. RecordPagePayload.timeline in @object-ui/types mirrors the response.
  • Deviations (named by the dev, accepted): plugin-detail.mdx updated. Two existing tests expected top: 200 on a page opening on the panel and now expect 5, which is the intended change. The refused-ledger case is pinned in the rest unit test only. The full app-shell suite was not run locally: the 151 importing files and every RecordDetailView* file were, and CI's Test UI covers the rest.
  • Tests: new record-page-timeline.dogfood, RecordDetailView.timelineCount-28737 and RecordTimelineTotal-28737, plus rest and spec cases. Derived gates: 108 of 108 exit 0, 0 NOT-MEASURED. check:generated passed.
  • Gates (check-runs on this head): 58 success, 7 skipped, 0 failed or pending. All ten required contexts succeeded.
  • Landing: ordinary queue landing, pr_ready then automerge_enable. Fixes #28737 closes the card on merge.

Generated by Claude Code

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 size/xl tests tooling

Projects

None yet

2 participants