Skip to content

feat(rest,console): a record open is one request; the page payload carries the record's streams and the related rows' names - #28682

Draft
objectstack-fleet[bot] wants to merge 8 commits into
mainfrom
claude/project-thread-iz2uqj
Draft

objectstack-fleet[bot] wants to merge 8 commits into
mainfrom
claude/project-thread-iz2uqj

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

Requested by Steedos · project thread

Fixes #28470

维护者速读

  • 改了什么:打开一条记录时,评论、动态、附件和关联行里引用记录的名称,都跟着 POST /data/:object/:id/page 一起返回;@ 提及候选人(sys_user?top=50)改为评论框第一次获得焦点时才读。打开记录不再单独发这些请求,也没有回退读取。
  • 为什么改:在 app-crm 的生产构建上实测,打开一个联系人除了 /page 之外还会发 sys_comment、sys_activity、sys_user?top=50,以及每个被引用对象一次 ?top=1 名称查询,共 8 个多余请求。
  • 风险与回滚:协议只增不减(Clause-② 加宽,Metadata-protocol: added,@objectstack/spec minor),新字段全部可选,旧客户端不受影响;每个新部件单独拒绝,不会连累整页。回滚就是还原本 PR。动态部件一次取 200 条,这是已命名的偏差,见下文。
  • 先看:packages/spec/src/api/record-page.zod.ts(新成员和流表说明)、packages/rest/src/record-page-routes.ts(每个部件就是它替代的那次读)、ui/packages/app-shell/src/views/RecordDetailView.tsx(何时请求各部件);文档 content/docs/frontend/plugins/plugin-detail.mdx 的 Console Integration 一节改为讲记录页请求。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/app-shell/src/views/RecordDetailView.recordOpenOneRequest-28470.test.tsx,五条通过:一次 /page,零次评论、动态、附件、提及候选和 ?top=1 读取,含两条消融。
  • 需要你做的:总监做合约审查(CONTRACT_REVIEW_TIER)。声明行已按席位修订的认领改为 Metadata-protocol: added,@objectstack/spec 定为 minor。PR 保持草稿。

What changes

One record open is one request. The parts a record page still read on its own after the page payload answered now ride in POST /api/v1/data/{object}/{id}/page, and the console takes them from there with no fallback read.

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

  • related[].expand?: string[]: the lookups of a related list's rows to resolve, as that list read's own $expand.
  • discussion?: boolean: ask for the record's comment thread and its reactions.
  • activity?: { top: int 1..200 } (RecordPageActivityRequestSchema, strict): ask for the newest activity rows and their count.
  • attachments?: { top: int 0..100 } (RecordPageAttachmentsRequestSchema, strict): ask for the newest attachments and their count.

Response (RecordPageResponseSchema):

  • discussion?: { comments, reactions } | { error } (RecordPageDiscussionSchema | RecordPageStreamRefusalSchema). reactions is { records } | { error } (RecordPageDiscussionReactionsSchema), so a refused reaction read leaves the comments answered.
  • activity?: { total, records } | { error } (RecordPageStreamRowsSchema | RecordPageStreamRefusalSchema).
  • attachments?: { total, records } | { error } (same pair).

New exports: RecordPageActivityRequestSchema, RecordPageAttachmentsRequestSchema, RecordPageStreamRefusalSchema, RecordPageStreamRowsSchema, RecordPageDiscussionReactionsSchema, RecordPageDiscussionSchema, RECORD_PAGE_MAX_ACTIVITY_ROWS (200), RECORD_PAGE_MAX_ATTACHMENT_ROWS (100), and for each schema its X (input) and XParsed (output) type.

@object-ui/types (ui/packages/types/src/data.ts, published):

  • RecordPagePayload.discussion?: { comments: Record<string, any>[]; reactions: { records: Record<string, any>[] } | { error: RecordPagePartRefusal } } | { error: RecordPagePartRefusal }
  • RecordPagePayload.activity?: { total: number; records: Record<string, any>[] } | { error: RecordPagePartRefusal }
  • RecordPagePayload.attachments?: { total: number; records: Record<string, any>[] } | { error: RecordPagePartRefusal }
  • The request type is the spec's RecordPageRequest, re-exported, so it gains the request members above with no separate declaration.

@object-ui/react: DiscussionContextValue.onMentionSuggestionsWanted?: () => void.

@object-ui/plugin-detail: RichTextCommentInput, RecordActivityTimeline and RecordChatterPanel take onMentionSuggestionsWanted?: () => void, called on the editor's focus. record:activity and record:chatter pass the context's hook.

What each new part answers

Each part is the read it replaces, asked as the same caller, behind the stream object's own API exposure gate, and refused on its own:

Part The read it replaces
related[].expand the related list read's own $expand (GET /data/{child}?$expand=…), spelled as the QueryAST relation map that read's normalizer lowers it to
discussion.comments GET /data/sys_comment where thread_id = {object}:{id}, oldest first, no page size
discussion.reactions GET /data/sys_comment_reaction where comment_id in those comments, oldest first; not asked when the thread is empty
activity GET /data/sys_activity for the record, newest first, $top, $count, $expand=actor_id
attachments GET /data/sys_attachment for the record, newest first, $top, $count

A record the caller cannot read still refuses the whole request, its thread with it. The body stays strict and the query-parameter set stays empty.

Console

  • The page asks discussion and activity when a block that draws the feed is on screen at open (the 动态 panel of a laid-out page, or a feed block on the tab the page opens on), and attachments when the page draws them. The feed, the Attachments panel and the Attachments tab badge draw those parts. A refused part draws the refusal the panel draws today.
  • The stream asks ride whichever payload the plan asks and never key it, so an assigned page landing after a synthesized one asks nothing again (objectui#11699 stays green).
  • Each related list asks the lookups of its rows to resolve (every declared reference of the child but the parent's, gated by field-level security), so the rows carry their names.
  • RelatedList's name fallback no longer resolves the parent record's own id, which no column draws.
  • The mention roster (sys_user, 50 rows) is read on the composer's first focus. A reply or an edit whose text holds an @ before then reads it when written, so its mentions still resolve.

Docs

  • content/docs/frontend/plugins/plugin-detail.mdx, the "Console Integration" section only (added to the file surface by claim amendment). It showed the console reading a record's comments with dataSource.find('sys_comment', …), with field names already stale on main. It now says a record open takes the discussion and the activity from the /page payload, and names when the console still reads sys_comment itself (a feed the open did not ask for, or a data source without getRecordPage) and when it reads the mention list. Its snippet calls getRecordPage with the real types. The rest of the page is unchanged.

The ?top=1 probe caller

RelatedList's reference-name fallback (ui/packages/plugin-detail/src/RelatedList.tsx) resolving the payload's bare ids in related rows, one GET /data/{object}?top=1&filter=["id","in",[…]] per referenced object, the parent FK itself among them. The rows now arrive expanded, and the parent FK is skipped.

Named deviation: the activity part asks 200 rows

The 动态 panel shows 3 or 5 rows, but its count chip and the 动态 tab count the whole merged stream (comments + activity + field changes, minus dropped types). A 5-row window would make that count wrong, so the part asks the same 200-row window the console's own read asked before. Shrinking it needs a server-side count of the merged stream: see out_of_scope_findings.

Follow-up: #28737 serves the merged 动态 count with the page payload, so the activity part can ask the panel's few rows. It owns RECORD_PAGE_MAX_ACTIVITY_ROWS.

/page once per open (card item 4)

Measured on a production build before this change: /page goes out once per record. The two other /page posts in a drawer open are the previous/next prefetch (#27056, #27545), different ids, by design. So item 4 changes nothing, and the seat recorded it out of scope.

App-level reads a record open causes (out of scope, left to #28085 / #27626)

  • In a drawer open: GET /api/v1/meta/object (the object list read again on open) and PATCH /api/v1/data/sys_user_preference/{id} (the recent-items write). Neither is the record's, and neither is changed here.
  • On the record route, the other 26 requests are the shell's own boot (runtime config, session, discovery, translations, organizations, auth config, the preference read, meta/app, meta/view, meta/page, meta/object, meta/dashboard, the app bundle, packages, permissions, localization, settings, AI agents, the approvals inbox, sys_inbox_message, sys_user/{me} and meta/object/sys_user) plus the same preference write. A record open does not cause them.

Before / after

Measured on examples/app-crm, production-built Console (pnpm build && pnpm console:build, pnpm dev:crm -- --fresh --ui --no-watch), Playwright, signed in as admin, every /api/ request from the click (drawer) or navigation (record route) until network idle plus 3 s. HotCRM was not available, so app-crm stands in.

Contact drawer (open "Ada Lovelace" from the Contacts list): 13 requests before, 4 after.

before                                                                      after
 1. GET   /meta/object                                                       1. GET   /meta/object
 2. POST  /data/crm_contact/{ada}/page                                       2. POST  /data/crm_contact/{ada}/page
 3. GET   /data/sys_user?top=50&select=id,name,email,image                      (read on the composer's first focus)
 4. GET   /data/sys_comment?sort=created_at&filter=thread_id = crm_contact:{ada}  (in /page: discussion)
 5. GET   /data/sys_activity?populate=actor_id&top=200&sort=timestamp&…         (in /page: activity)
 6. POST  /data/crm_contact/{prev}/page     neighbour prefetch               3. POST  /data/crm_contact/{other}/page   neighbour prefetch
 7. POST  /data/crm_contact/{next}/page     neighbour prefetch
 8. GET   /data/sys_organization?top=1&filter=id in [...]                       (related rows arrive expanded)
 9. GET   /data/sys_user?top=1&filter=id in [...]                               (related rows arrive expanded)
10. GET   /data/crm_contact?top=1&filter=id in [{ada}]                          (the parent FK: no longer resolved)
11. GET   /data/crm_account?top=1&filter=id in [...]                            (related rows arrive expanded)
12. GET   /data/crm_opportunity?top=1&filter=id in [...]                        (related rows arrive expanded)
13. PATCH /data/sys_user_preference/{id}                                     4. PATCH /data/sys_user_preference/{id}

The after run has one neighbour prefetch instead of two only because the fresh seed listed Ada third of three (no next row); the prefetch is unchanged.

Account drawer (open "Acme Corp"): 13 requests before, 5 after: GET /meta/object, POST /data/crm_account/{acme}/page, the two neighbour prefetches, PATCH /data/sys_user_preference/{id}. Gone: sys_user?top=50, sys_comment, sys_activity, and the five ?top=1 probes (sys_organization, sys_user, crm_contact, crm_account, crm_opportunity). The page request body:

{"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id"],"related":[{"object":"crm_activity","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id","contact","opportunity"]},{"object":"crm_contact","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id"]},{"object":"crm_lead","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id","converted_opportunity"]},{"object":"crm_opportunity","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id","renewal_of"]}],"explain":["update","delete","transfer"],"fieldAccess":true,"approvals":true,"discussion":true,"activity":{"top":200}}

Contact record route (navigate to /apps/crm_app/crm_contact/record/{ada}, shell boot included): 37 requests before, 28 after. The nine gone: sys_user?top=50, sys_comment, sys_activity, the five ?top=1 probes, and GET /meta/object/sys_organization (the schema read the name fallback made for the sys_organization probe). Nothing was added.

Composer focus (account drawer, then a click into "Write a note… @ to mention"): one GET /data/sys_user?top=50&select=id,name,email,image, at the focus, and none before it.

App-crm does not enable files on these objects, so attachments is not asked here; the console test and the dogfood suite cover that part.

Tests

  • packages/spec/src/api/record-page.test.ts: request strictness for the new members (unknown key in activity / attachments refused, bounds), and each stream part is its rows or its refusal, never both and never neither.
  • packages/rest/src/record-page-routes.test.ts (7 new, 20 total): each part's read as replaced, for the same caller; an empty thread asks no reaction read; per-part refusal (a refused activity read leaves the others answered); a refused comment read; a refused reaction read keeps the comments; the stream object's exposure gate; strict body.
  • packages/qa/dogfood/test/record-page-streams.dogfood.test.ts (new, real HTTP + security stack, non-admin member): discussion, reactions and activity equal the member's own GET reads byte for byte, including a top: 1 window with the whole count; a private record refuses the whole request as GET does; attachments count and list only the record's own (3), never another record's (2 secret).
  • packages/qa/dogfood/test/record-page.dogfood.test.ts: a related list's expand is its read's own $expand.
  • ui/packages/app-shell/src/views/RecordDetailView.recordOpenOneRequest-28470.test.tsx (new, real ObjectStackAdapter, StrictMode): one /page; zero sys_comment, sys_comment_reaction, sys_activity, sys_attachment, sys_user?top=50 and ?top=1 reads; the body asks discussion, activity 200, attachments 100 and the related expand; the feed, the attachment and the badge draw the payload; the mention read goes out once, on the composer's focus; a refused activity draws data-denied-source="activity" and is not read again. Ablations: with no getRecordPage the detectors see the sys_activity, sys_attachment and sys_comment reads; with the related expand ignored they see the ?top=1 name probe. Removing the parent-FK skip was checked to go red, then restored.
  • Updated to the payload behavior: RecordDetailView.recordPagePayload-27627, RecordDetailView.recordOpenWaves-13010, RecordDetailView.withheldMentions-28218 (the composer reads the roster on focus; a reply reads it at the write).
  • ui/apps/console/src/services/approvalRequestsDataSource.servedPage.test.tsx: it pinned the related timeline's label read of its parent request_id, which the related expand retires. It now asserts no read of the parent request by id, and still that no read the page makes is refused.

Acceptance notes

  • Contract tests: each new part answers the read it replaces for the same caller (route unit tests + dogfood as a member); a refused part is refused on its own; the body is strict.
  • Console test: one /page, zero sys_comment, sys_activity, sys_attachment, sys_user?top=50 and ?top=1 reads, with ablations.
  • Before/after request lists: above.
  • Preview check is the epic seat's.

Deviations

  • The activity part asks 200 rows, as named above.
  • The discussion part is the whole thread, with no page size. The card asked for the discussion's first page. The part answers the read it replaces: on main the console read the whole thread (sys_comment where thread_id is {object}:{id}, oldest first, no $top) and held all of it, and the comment thread has no paging door today. A page size needs that door first, which is outside this card.

out_of_scope_findings

  • class b, reach: every record page with a 动态 panel or tab — a server-side count of the merged 动态 stream (comments + activity + field changes, minus the types the panel drops), so the activity part can ask the panel's 3 or 5 rows instead of 200 and the count chip still holds. Today the count is the client's, over the rows it holds, which is why the part asks 200.

Verification

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 108 commands, all exit 0 without a PR body (check-plugin-teardown-shape --self-test after fetching its pinned fixture commit on this shallow clone; pnpm --dir ui exec vitest run scripts/ rerun alone after two 15 s timeouts under a concurrent spec suite: 169 files passed).
  • @objectstack/spec: test 782 files / 23126 tests passed; test:repo 87 files / 1448 passed; check:generated current after the merge of main.
  • @objectstack/rest: typecheck (incl. test typecheck) 0; record-page-routes.test.ts 20 passed.
  • packages/qa/dogfood: typecheck 0; the two record page suites 8 passed against the rebuilt rest dist.
  • ui: 330 test files that import the touched modules, all passed (--maxWorkers=2 under the verify lock, the record-open files rerun after their updates); type-check 0 for @object-ui/types, @object-ui/react, @object-ui/plugin-detail, @object-ui/app-shell; eslint 0 errors on every touched file (root config for the platform files); node scripts/check-sdui-manifest.mjs 0.
  • Merged origin/main (no text conflicts; generated spec artifacts regenerated, check-regen-pending clear). At the merged head: the five record-open ui files (42), the route suite (20) and record-page.test.ts (7) rerun green.
  • Declaration lines per the amended claim (Metadata-protocol: added, @objectstack/spec at minor in .changeset/28470-record-page-streams.md, head 579bcda1). PR-body gates with this body as GITHUB_EVENT_PATH: check-changeset-no-major --base origin/main 0 (level axis: widening accounted for at minor), check-adr-0087-registration 0, check-closing-keyword-parity 0, check-empty-changeset 0, check-partof-closing-keyword 0; pnpm check:changeset-fixed 0.
  • Two CI reds at 579bcda1, fixed at 2369096b. CI's Test Core 8/8 runs rest's test:repo, whose query-slot census (rest-server-canonical-query-ast.test.ts) counts every query: token in a REST registrar: the stream read's parameter was spelled query, so it is renamed and record-page-routes.ts is pinned at 2 slots (total 5 → 6). CI's Test UI 9/16 ran the console served-page test above. At 2369096b: rest test and test:repo green, the 26 console test files that touch the record page green.
  • CI at 2369096b: 65 check runs, 58 success, 7 skipped, none failed; Check Changeset green.
  • Contract review PASS at f2e04ba8 (no must-fix). Its should-fixes: @objectstack/rest is graded minor in .changeset/28470-record-page-streams.md at 3b6fec52, as AGENTS.md's Post-Task Checklist step 3 reads ("a kind takes at least minor on a package the diff moves") and as the door's first changeset graded it (spec and rest share one fixed group, so the released version is unchanged; check-changeset-no-major --base origin/main, check:changeset-fixed, check-empty-changeset 0); the whole-thread discussion is named under Deviations; the 200-row cap's follow-up card is spec+runtime+ui: the record page payload serves the merged 动态 count, so the activity part asks the panel's few rows instead of 200 (RECORD_PAGE_MAX_ACTIVITY_ROWS owner) #28737.
  • Docs fix at f2e04ba8: the section's tsx snippet type-checks against @object-ui/types (tsc on the extracted fence, 0; a wrong activity.top type in it goes red, TS2322). The docs gates the derived list names, all exit 0: check-docs-section-name (and --self-test), check-affected-docs, check-drift-comment, check-doc-route-spelling --advisory, check-doc-authoring, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check:docs-spec-enumerations, check:docs-transcript-drift, check:issue-citations, check:role-word, check:nul-bytes, check:org-identifier.

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — no file under skills/** teaches the record page payload (POST /data/{object}/{id}/page), its request or response members, or when the console reads the mention roster (a git grep of skills/ for RecordPage, getRecordPage, sys_comment, sys_attachment and mentionSuggestions names nothing). skills/objectstack-data teaches enable.activities: false hiding the timeline and the detail-page related lists by relationship flags; both stay true.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC

… and the related rows' names

The page names a related list's expand and asks for the discussion, the
activity and the attachments; the route answers each as the read it
replaces, behind that object's own exposure gate, refused on its own.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
The record page asks its payload for the discussion and the activity when a
block that draws the feed is on screen at open, and for the attachments when
the page draws them. Each related list asks the lookups of its rows to
resolve, as its own read expands them, and the related list's name fallback
no longer resolves the parent record's own id. The mention roster is read on
the composer's first focus, or by a reply or edit whose text names someone
before then. Stream asks ride the payload the plan asks and never key it, so
an assigned page landing after a synthesized one asks nothing again.

Regenerated spec artifacts for the record page schema change; two changesets.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
…on map

The related list part and the activity part pass `expand` as the relation
map the list read's normalizer lowers `$expand=a,b` to, so the rest package
type-checks and the rows expand as that read's rows do.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
Generated spec artifacts regenerated from the merged tree.

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

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/spec, touching 39 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 fa494d0538fe69e19be4a21ab13b92e8e6ae8d23.

⛔ 9 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
  • 3 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 42 pages), /data/{child} (route, 60 pages), /data/{object} (route, 75 pages)
  • 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 — 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 fa494d0538fe69e19be4a21ab13b92e8e6ae8d23 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json fa494d0538fe69e19be4a21ab13b92e8e6ae8d23

⚠️ 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 fa494d0538fe69e19be4a21ab13b92e8e6ae8d23 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…-protocol added, spec minor

The amended claim reads the 17 new api/RecordPage* authorable keys as a
metadata-protocol addition, so @objectstack/spec moves to minor.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
…ted query slot

The stream reader's parameter no longer spells `query:`, so the REST query
AST census counts one inline-literal slot for it; the record page module's
floor moves 1 -> 2 and the registrar sum 5 -> 6. The approval request
page's served-page test pins that the timeline asks no label read of its
parent `request_id`, which no column draws, and that its actor lookup is
still labelled.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
… record page payload

The "Console Integration" section still showed the console reading a
record's comments with `dataSource.find('sys_comment', …)`, with field
names already stale. A record open now asks the discussion and the
activity in its one `POST /data/{object}/{id}/page` request; the section
says so, and when the console still reads `sys_comment` on its own.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head f2e04ba8ef60e9c213b0bc9214832c1d746fddc5 · 2026-11-01T15:54Z

Taken under references/contract-review.md (b): this head has waited a full triage round with no record, claim, hold or hand-off. The review runs at CONTRACT_REVIEW_TIER.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f2e04ba8ef60e9c213b0bc9214832c1d746fddc5
Local-runs: none
Scope: packages/spec/src/api/record-page.zod.ts + packages/rest/src/record-page-routes.ts, packages/rest/src/record-page-routes.test.ts, packages/rest/src/crud-routes.ts (the GET /data/:object door the parts claim parity with), packages/rest/src/rest-server.ts (apiAccessDenialFromEnable, enforceApiAccess), packages/objectql/src/engine.ts (the expand sub-read), packages/qa/dogfood/test/record-page-streams.dogfood.test.ts, ui/packages/app-shell/src/views/RecordDetailView.tsx, ui/packages/app-shell/src/views/recordPagePayload.ts, ui/packages/app-shell/src/views/RecordAttachmentsPanel.tsx, ui/packages/types/src/data.ts, ui/packages/components/src/hooks/related-count-store.ts, ui/packages/components/src/renderers/layout/containers.tsx

Inputs read: card #28470 (body and all 7 comments, including the three claim amendments and the os-dev-report), PR #28682 (body, 34-file list, the docs-drift comment, no review comments), the net diff origin/main...f2e04ba8 (34 files, +1860 / -169), the 69 check-runs on the head, and the context on the same door: #27713, #28546, #28657 and the merged #28708.

Checks on the head: 69 check-runs, none failed, none in progress. The skipped ones are opt-in or matrix placeholders (Packed-tarball smoke, 18.x upgrade harness, Temporal Conformance, Dogfood Verify CLI, the Dogfood matrix stub, UI Published Artifacts, Schedule gate) plus Auto Label and Check PR Size on a second trigger of the same workflow, which succeeded on the first. Check Changeset is green on both triggers. The commit-status API reports pending with zero statuses, which is the empty default, not a pending check. All of them belong to this PR's head.

PR author is objectstack-fleet[bot]; no login note is owed.

① Derived judgments

Request, RecordPageRequestSchema (still .strict(), the query-parameter set still empty):

  • related[].expand?: string[] (each min(1), no array cap). Named right. The route lowers it to the relation map { rel: { object: rel } }, the same spelling the export route and the list normalizer use for $expand=a,b, so a list's rows expand as its own GET /data/{child}?$expand= read does. No cap on the array, as the record-level expand already has none. Consistent, noted.
  • discussion?: boolean. Named right. A non-boolean is refused (spec and route tests).
  • activity?: { top: int 1..200 }, strict. Named right. min(1) means a count-only activity part cannot be asked, where attachments allows top: 0. An asymmetry, not a defect: the console never asks a count alone here.
  • attachments?: { top: int 0..100 }, strict. Named right; 0 is the count alone, as related[].top already reads.

Response, RecordPageResponseSchema (a non-strict object, as before):

  • discussion?: RecordPageDiscussionSchema | RecordPageStreamRefusalSchema. Both arms strict, so a refused stream cannot parse as an empty one. reactions is its own union { records } | { error }, so a deployment without sys_comment_reaction answers its 404 inside the part and the comments stay answered. Named right; the spec test pins "rows or refusal, never both, never neither" for all three parts.
  • activity? and attachments?: RecordPageStreamRowsSchema { total, records } | RecordPageStreamRefusalSchema. Named right.
  • RecordPageStreamRefusalSchema is RecordPagePartErrorSchema made strict. Two names for one shape; the strict copy is what makes the unions disjoint, so it earns its place.
  • Each new response key appears only when the request asked for it. A request from an old client (no discussion, activity, attachments, no related[].expand) receives the pre-PR answer byte for byte: record, related rows with bare ids, the three optional parts as before. Kept.
  • Order of the activity part: timestamp desc with top. The console's own read on main asked asc with $top: 200. For a record with more than 200 rows the payload now carries the newest 200 instead of the oldest 200, and the console's fallback read was changed to desc too. A deliberate change, documented in the stream table, and the right one for a feed. Noted.
  • Discussion: thread_id = {object}:{id}, created_at asc, no page size. The console wrote and read exactly this thread id and sent no $top, and the engine applies no default limit to a find without one (engine comment: limit carries no .default()), so the part is the same read. See ③ and ④ for the size question.
  • New exports: 6 schemas, 2 constants and their X / XParsed types, all listed in api-surface/api.json (20 lines) and export-origins/api.json. authorable-surface/api.json gains 14 api/RecordPage* keys; the os-dev-report wrote 17. The diff is 14. Prose only, the artifacts are generated and check:generated passed in CI.
  • The spec module's header and the stream table on RecordPageActivityRequestSchema match the route line for line (filters, orders, count, actor_id expand). Named right.

② Semver level

  • Declaration lines on the PR body and on .changeset/28470-record-page-streams.md: Clause-②: yes (widening), Metadata-protocol: added. They match the seat's amendment 6558547648 on the card. added is right: 14 new authorable keys enter the api shard, nothing existing moves meaning.
  • @objectstack/spec: minor. Right for added.
  • @objectstack/rest: patch in the same changeset. AGENTS.md rule 3 reads "a kind takes at least minor on a package the diff moves". check-changeset-no-major mechanizes the softer form "at least one moved package minor+" and passed. The route gains four new accepted body members and three new answer members, an additive feature on a published package, and feat(rest,console): one request opens a record page #27713, which opened this very door, graded @objectstack/rest minor under the same kind. Should-fix: lift @objectstack/rest to minor, or have the seat record that the gate's "at least one" reading governs over the rule text. Not a FAIL: the gate accepted it and the published behaviour is unaffected.
  • .changeset/28470-console-record-open-one-request.md: four @object-ui/* packages at patch under Clause-②: yes (widening), Metadata-protocol: none. Consistent with its own line: no metadata key moves there, and RecordPagePayload is a TypeScript type for console consumers, not an authorable protocol. The two changesets carry different Metadata-protocol lines; the PR body carries the stronger one, which is what the gate reads. Right.

③ Boundary flags

Dev deviations, from the os-dev-report (card comment 6558534355) and the PR body:

  1. Declaration lines versus the changeset gate. Answered: the seat amended the claim to Metadata-protocol: added, @objectstack/spec minor (card comment 6558547648); commit 579bcda1 carries it; Check Changeset is green on the head.
  2. The activity part asks 200 rows, where the card said "at the size the 动态 panel draws, not 200". Named in the report, the PR body and the schema constant's docblock. The reason holds: the 动态 count chip and tab count the merged stream client-side over the rows held, so a 5-row window would make the count wrong. The deviation is accepted as named. Should-fix: file the class-b out-of-scope finding (a server-side count of the merged stream) as a card before landing, so RECORD_PAGE_MAX_ACTIVITY_ROWS = 200 has an owner; shrinking that cap later is a narrowing and needs its own review.
  3. Files touched beyond the amended surface: RecordAttachmentsPanel.tsx and four test files. RecordAttachmentsPanel.tsx is in the original claim's file surface (card comment 6556969643), so it is in surface; the tests pin retired reads beside the files they test. Answered.
  4. A reply or edit with an @ before the composer was ever focused reads the roster at the write. Right: without it a mention would resolve to nobody once the open stopped reading sys_user. Pinned in withheldMentions-28218. Answered.
  5. No assignees or reviewers on the PR. Process, per the seat's rules. Answered.

open_questions: one, "amend Metadata-protocol to added or accept the red". Answered by the seat's amendment 6558547648 and carried on the head.

Flags the report did not raise:

  1. The discussion part is the whole thread with no page size. The card's Target 1 asked for "the first page of the discussion". The dev matched the console's existing unbounded read (parity, see ①) but did not name the gap. Should-fix: name it in the PR body's Deviations with the reason (the feed draws the whole thread and has no paging door today), so the card's wording and the shipped contract agree on record.
  2. Acceptance 3 asked for before and after lists on HotCRM contact John Smith. The PR measured examples/app-crm on a production build and says HotCRM was not available. The lists are complete (URLs, counts, the request body) and the card's own Measured section names app-crm as the same shape. Accepted as named; the epic seat's preview check (Acceptance 4) is still outstanding and is not this review's.
  3. Card item 4 (/page twice): measured once per open on a production build; the other posts are the prev/next prefetch. Answered as the card prescribed ("record that in the PR and change nothing").

④ Design judgment

The hit file is judged against the card's Wanted list, #27627's rule "every part is the read it replaces, as the same caller", and its first consumer, the console's RecordDetailView.

Read authority. The route reads the record first through getData as the caller; its refusal is the whole answer before any stream is asked (the dogfood pins page equal to the member's own GET 404, byte for byte). Each stream is findData over its own object with the caller's context, after apiAccessDenialFromEnable(item.enable, name, 'list'), which is the same verdict enforceApiAccess('list') renders on GET /data/{object} (the extra API-key credential check there applies to writes on sys_api_key only). A stream object the deployment does not have throws OBJECT_NOT_FOUND into the part's refusal, as the list door 404s it. Row-level and field-level security are the engine's, applied inside findData as on the GET. The dogfood test compares the member's part against the member's own GET reads, rows and total included, and asserts another record's attachments are neither counted nor listed. The filters are pinned to the route's own {object} and {id}, which the record read has just proven readable, so a stream cannot be aimed at another record. The reactions read is scoped to the comment ids the caller just received. I find no way the payload reads a row the caller's scope refuses on the data door.

related[].expand and read authority. The engine's expand sub-read re-enters the security middleware for the referenced object with the full treatment (CRUD gate, requiredPermissions, RLS, FLS, since #7626), and a refusal leaves the bare id in place. So the rows carry only referenced records the caller could read directly, which is what the schema's description promises. The API exposure flag of the referenced object (enable.apiEnabled) is not consulted on an expansion, but that is the published behaviour of GET /data/{child}?$expand= today, so the payload opens no door the list door did not already hold. Not a widening of read authority. Noted so the next reviewer of the expand path knows where the line sits.

The approvals door. #28546 and #28657 were the same class, an approvals row read through a generic data-scoped door. This PR does not touch that line: RecordDetailView still asks no payload for sys_approval_request, and the routed source still refuses getRecordPage for it. The new parts ride only a payload the plan already asks, so a request page asks none of them. Right.

Per-part refusal. A refused activity read leaves the record, the discussion, the attachments and the related lists answered (route test and console test, data-denied-source="activity"). A refused comment read refuses the discussion alone and asks no reaction read. A refused reaction read keeps the comments. The three unions are disjoint by strictness, so no consumer can read a refusal as an empty stream. Holds.

Response size. Per open the payload now carries up to 200 activity rows with the actor expanded, up to 100 attachment rows, the whole comment thread and its reactions, plus every related list's rows with their lookups expanded. These are the same bytes the console fetched in separate requests before; the one body is the card's point. The one unbounded term is the thread (flag 6). The neighbour prefetch now asks the same streams for the previous and next records, so a drawer open reads three records' streams rather than one. That is a console cost decision outside the hit file; recorded here for the epic seat's preview check, not a finding.

Stale counts against the related-list filter. The attachments total is seeded into RelatedCountStore under the key the Attachments tab badge probes (attachments:{object}, filter undefined), and the badge's probe wrapper injects the same two-key scope { parent_object, parent_id } the route filters by, so badge and panel answer one question. takeRows hands the rows over only when the seeded top covers the panel's limit (100 against 100) and the order matches (undefined on both). After an upload, a delete or Retry the panel reads for itself. A related list's total is unchanged by this PR; expand touches rows only. Holds.

Strictness. The body is strict at every level; a window past its cap, an undeclared key in a part, a non-boolean flag and a non-array expand are each a 400 before any read (route and spec tests). The query-parameter set is still empty.

The first consumer. The console asks discussion and activity only when a feed block is on screen at open and attachments only when the page draws them; the parts are taken off the payload once and never re-read on the open; a part the payload did not carry is read as before. The mention roster moves to the composer's first focus. The one-request test asserts one /page and zero sys_comment, sys_comment_reaction, sys_activity, sys_attachment, sys_user?top=50 and ?top=1 reads, with two ablations that make the detectors fire. Acceptance 1 and 2 of the card are met by tests on the head.

No must-fix. Three should-fixes: ② the @objectstack/rest level, ③-2 the follow-up card for the 200-row cap, ③-6 the unnamed whole-thread deviation.

Implemented-by: claude/project-thread-iz2uqj
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

…ck/rest minor

The route gains four accepted body members and three answer members, an
additive feature on a published package; the contract review asked the
level to read `minor`, as the door's first changeset graded it. Both
packages sit in one fixed group, so the released version is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 3b6fec5285b7b84331947ae2750103972241ac43 · 2026-11-01T17:53Z

The head moved past this seat's own record on this PR, so the delta re-review is this seat's (references/contract-review.md). It runs at CONTRACT_REVIEW_TIER.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3b6fec5285b7b84331947ae2750103972241ac43
Local-runs: none
Scope: packages/spec/src/api/record-page.zod.ts + .changeset/28470-record-page-streams.md (the one file the interdiff moves), .changeset/config.json (the fixed group the commit message cites), packages/rest/src/record-page-routes.ts (the door the grade is about), AGENTS.md Post-Task Checklist step 3 (the rule the grade follows)

Delta record. It re-reviews the interdiff from f2e04ba8ef60e9c213b0bc9214832c1d746fddc5 (this seat's PASS, comment 6560407872) to the head above. The previous record's judgments stand where the delta does not touch them; this record says which.

Inputs read: the compare f2e04ba8...3b6fec52 (1 commit, 1 file, +1 / -1, no merge commit), the head commit message, the PR body as it stands on the head, the PR comments since the previous record (the docs-drift check and this seat's claim; no review comments, no reviews), the two changesets and .changeset/config.json at the head, AGENTS.md at the head, card #28737, the 69 check-runs on the head, and the 34-file list of the PR.

The interdiff

  • One commit, 3b6fec52, parent f2e04ba8. Not a merge. There is no main merge in the delta, so no merge changed the PR's hunks.
  • One file: .changeset/28470-record-page-streams.md. The line '@objectstack/rest': patch becomes '@objectstack/rest': minor. Nothing else moves. The spec hit file, the route, the tests, the console and the docs are byte for byte the files the previous record reviewed.
  • The PR's file list is still 34 files. The delta adds none and removes none.
  • The PR body was edited since the previous record. It now carries a Deviations section with two entries, a Follow-up line naming spec+runtime+ui: the record page payload serves the merged 动态 count, so the activity part asks the panel's few rows instead of 200 (RECORD_PAGE_MAX_ACTIVITY_ROWS owner) #28737, and a Verification line recording the minor grade at 3b6fec52. The declaration lines are unchanged: Clause-②: yes (widening), Metadata-protocol: added.

Read authority, re-checked

The delta touches no stream, no related block, no route and no schema. The spec hit file and packages/rest/src/record-page-routes.ts are identical between the two heads (the compare lists neither). So the previous record's reading holds without change: every stream is read as the caller through findData with the caller's context, after apiAccessDenialFromEnable(item.enable, name, 'list'), the same verdict GET /data/{object} renders; the record read comes first and its refusal refuses the whole request; each part is refused on its own and the three unions are disjoint by strictness; total is counted inside the same findData the rows come from, so it counts no row the caller cannot read; related[].expand goes through the engine's expand sub-read with the CRUD gate, RLS and FLS, and a refusal leaves the bare id. Nothing in the delta widens what a member can read through the page payload.

The old clients' answer. The request and response schemas did not move. A request with none of discussion, activity, attachments or related[].expand still receives the pre-PR answer byte for byte. Holds.

① Derived judgments

Unchanged by the delta. The interdiff adds no member, removes none and changes no description. Every ① judgment of the previous record stands as written: the four request members, the three response members, the strict refusal shape, the exports and the generated surface artifacts.

② Semver level

  • @objectstack/spec: minor, @objectstack/rest: minor in .changeset/28470-record-page-streams.md at the head. The declaration lines Clause-②: yes (widening) and Metadata-protocol: added are on the changeset body and on the PR body. They match.
  • The lift to minor is right. AGENTS.md step 3 at the head reads: "a kind takes at least minor on a package the diff moves". The diff moves @objectstack/rest (the route gains four accepted body members and three answer members), and the kind is added. feat(rest,console): one request opens a record page #27713, which opened this door, graded @objectstack/rest minor under the same kind. The grade now follows both the rule text and the precedent.
  • The commit message says the released version is unchanged because both packages sit in one fixed group. Checked: .changeset/config.json at the head has one fixed group of 109 members, and @objectstack/spec and @objectstack/rest are both in it. A fixed group bumps every member to the highest level in the group, and spec was already minor, so the lift changes the changelog line and not the version. Right.
  • Check Changeset is green on both triggers of the head. So check-changeset-no-major accepts the new grade.
  • .changeset/28470-console-record-open-one-request.md is unchanged: four @object-ui/* packages at patch under Metadata-protocol: none. The previous record's judgment of it stands.

③ Boundary flags

The three should-fixes of the previous record:

  1. ②, the @objectstack/rest level. Addressed. patch became minor in the changeset at 3b6fec52, with the rule and the precedent cited in the commit message, and the fixed-group reasoning holds.
  2. ③-2, a follow-up card for the 200-row activity cap. Addressed. spec+runtime+ui: the record page payload serves the merged 动态 count, so the activity part asks the panel's few rows instead of 200 (RECORD_PAGE_MAX_ACTIVITY_ROWS owner) #28737 is open (p1, finding, domain:ui, area:records). Its body names RECORD_PAGE_MAX_ACTIVITY_ROWS as what it owns, asks for a server-side count of the merged stream, and says that lowering the cap below 200 is a narrowing that needs its own contract review. The PR body references it under the named deviation. Right on both points.
  3. ③-6, the whole-thread discussion part unnamed. Addressed. The PR body's Deviations section now names it: the card asked for a first page; the part answers the read the console made on main (whole thread, oldest first, no $top); the comment thread has no paging door today, and a page size needs that door first. That is the reason the previous record asked for.

No new dev flags. The os-dev-report was not re-issued; the PR body's own Verification lists the three fixes. The card #28470 has no comment since the previous record.

PR author is objectstack-fleet[bot]; no login note is owed. The PR is still a draft, as its body says it should be.

④ Design judgment

The delta carries no design change. The hit file packages/spec/src/api/record-page.zod.ts is identical to the one the previous record judged, as is its first consumer and the route. The previous record's ④ stands in full: read authority, the expand path, the approvals door untouched, per-part refusal, response size, the seeded attachment count, strictness and the first consumer. The one term in ④ the delta touches is the response-size note on the unbounded thread, which is now a named deviation on the PR body (③-3 above) and has its paging door recorded as outside this card. That closes the note; it was never a must-fix.

Checks on the head

69 check-runs on 3b6fec5285b7b84331947ae2750103972241ac43, all completed: 60 success, 9 skipped, none failed, none in progress. The skipped ones are opt-in or matrix placeholders (18.x upgrade harness, Packed-tarball smoke, Temporal Conformance, Dogfood Verify CLI, the Dogfood matrix stub, UI Published Artifacts, Schedule gate) and Auto Label and Check PR Size on the second trigger of their workflow, which succeeded on the first. Check Changeset succeeded on both triggers. Every run names this head. The commit-status API reports pending with zero statuses, which is the empty default and not a pending check. Nothing red, nothing pending.

Which judgments stand

① stands whole. ② is re-rendered here: the one changed line is the grade, now right. ③ is re-rendered for the three should-fixes, all addressed; the previous record's ③ items 1, 3, 4, 5, 7 and 8 stand. ④ stands whole; the delta touches none of the code it judged.

No must-fix. No should-fix open.

Implemented-by: claude/project-thread-iz2uqj
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

This branch has not been deployed

No deployments
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

1 participant