Skip to content

approvals B1 (child of objectui#2763): the sys_approval_request detail page on the standard record renderer, composed from A1's data source, A2's record card, A3's progress widget, the action timeline and DeclaredActionsBar #12045

Open

Description

@objectstack-fleet

Ruled: 6079807016 · letter 乙 · 2026-10-09T11:18Z

Blocked-by: #12484

This line gates the page-mount step (ruling 6079807016). The other two upstreams have closed:

objectui's move to next (#12030) is what remains. The build round landed as a368ccb1. The page-mount step also carries objectui#12077 (naming the pending approvers), which is pm:blocked on this card.

Filing gate ④ — a coordination node: a child of the cross-layer parent objectui#2763. Parent: objectui#2763 (pm:epic). The maintainer's ruling there (5166015115): A2 first, then the A children, with B1–B3 following in order. All three A children have landed:

  • A2, objectui#12029 (049012b): RecordPreviewCard;
  • A3, objectui#12033 (ba9e820): DecisionProgressIndicator;
  • A1, objectui#12032 (d99b731): createApprovalRequestsDataSource, with ApiDataSource.findOne narrowed to 404.

Who acts on it: the objectui domain:ui execution seat 3 (seat post objectui#9800), on the maintainer's instruction in that session: 「UI 还有很多任务,为什么不派发?包括epic 也可以派发」.

What B1 is (from the parent's scope, verbatim)

B1 · sys_approval_request detail page — sections (request fields), the polymorphic target-record card (A2), the progress widget (A3), a sys_approval_action related-list (timeline), and DeclaredActionsBar; data via A1.

And the parent's honest caveat, which the maintainer's ruling repeats: prove that ActionParamDialog can host the decision composer (a comment, several attachments, and the timeline's context). If it cannot, keep one thin custom approve/reject action, a component-level exception and not a hand-rolled page.

Measure first: the shape is open on current main (d99b731f)

  • Where the page's metadata lives. objectstack's plugin-approvals ships the sys_approval_request object and its list views, but no page for it (source search on objectstack main). Today the standard RecordDetailView synthesizes a record page from the object definition. Measure which route B1 takes:

    • (a) a page objectui composes from the synthesized record page plus the three components, mounted where the inbox's drawer is today; or
    • (b) page metadata authored in objectstack's plugin-approvals, which is objectstack domain:services work and a cross-repo child with Blocked-by: on this card.

    ⛔ Do not author objectstack metadata from this card.

  • Component types. For page metadata to reference A2 and A3, they must be registered as component types. Measure whether objectui's page renderer accepts a newly registered type, or whether @objectstack/spec's page schema restricts component types to a declared set. A spec declaration is v18 work for the spec seat (objectui resolves ^17 until 18.0 GA): Blocked-by: on a spec-lane card.

  • The composer. The ActionParamDialog proof above.

Carried from the A children (on the parent's checklist)

  • From A2 (6076152376): the card is cause-free, and a platform-asserted deletion (an approval's cancelled + record_deleted) stays the caller's to render. B1 decides how this page renders it.
  • From A1 (6077174661, 6077915878): the get carries viewer and decision_progress for pending requests. The routed source sends side reads (sys_user, comments, activities, history) to the host. A refused or failed get now shows RecordDetailView's own states.

Duplicate check

  • Semantic issue search on objectstack-ai/objectui (open and closed), sys_approval_request detail page metadata record page approvals timeline DeclaredActionsBar: 9 hits (1 open, 8 closed). The open hit is the parent objectui#2763. The closed hits are inbox-drawer defects (objectui#11677, objectui#5553), record-page approval entries (objectui#3055, objectui#3461), the polish card objectui#2762, and two unrelated cards (objectui#10224, objectui#8442). None is this page.

Dedupe words: approval request detail page standard renderer · sys_approval_request record page metadata · approvals composer ActionParamDialog


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:workflowApprovals and automation — the work that runs without a person driving it
    on Oct 9, 2026
  2. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3
    Session: session_01CGZy1BGCjdN5cXqL9cnvB8
    Account: os-support-ai
    Branch: claude/issue-12045-approval-detail-page
    Worktree: objectui-issue-12045
    Domain: domain:ui
    Seat: domain:ui#3
    Authority: an epic child (pm:epic stays on it). The maintainer directed this seat to dispatch epics, in this session, verbatim: 「UI 还有很多任务,为什么不派发?包括epic 也可以派发」.
    File surface (on main d99b731f). This round is measurement only: no PR, and no file changes on the branch. The card's three open shape questions get measured answers and options first:

    • where the page's metadata lives: (a) composed in objectui over the synthesized record page, or (b) authored in objectstack's plugin-approvals;
    • whether @objectstack/spec's page schema lets page metadata reference a newly registered component type (A2, A3), or needs a declaration;
    • whether ActionParamDialog can host the decision composer (a comment, several attachments, timeline context), or one thin custom approve/reject action is needed.

    The build surface is written by a claim amendment once the seat has answered, or the maintainer has where the answer is theirs. Reads anywhere in objectui and objectstack are fine; scratch probes stay uncommitted and are deleted.

    ⛔ Not on it:

    • any edit to objectstack (plugin-approvals, @objectstack/spec);
    • any objectui source edit this round;
    • B2 (the list view) and B3 (deleting the inbox);
    • packages/components/src/ui/** and packages/i18n/**.

    Container & model: L, mode:subagent (an in-session subagent), model: opus (dispatch-gates --tier --repo objectstack-ai/objectui over these paths: no path-derived mandate; default tier). The contract review runs at the ceiling, CONTRACT_REVIEW_TIER, on the build round, because of the Clause-②: yes line below.
    Clause-②: yes
    Responsibility: objectui and objectstack: the approval request's detail is a hand-rolled drawer in the console, so every defect of the standard record renderer's own features (badges, action hierarchy, i18n, layout) recurs there | the platform path: the standard record renderer, A1's approvals data source, A2's record card, A3's progress widget, sys_approval_action as a related list, and DeclaredActionsBar | every approver and submitter who opens a request
    Thread-read: none
    Serial constraints cleared: none blocking (measurement only).

    • Parent objectui#2763: A1, A2 and A3 have landed (d99b731, 049012b, ba9e820).
    • Open objectui PRs, read 2026-10-09T09:34Z: objectui#12041 (objectui#6349 batch 4, core and types) touches no approvals file.

    Why Clause-②: yes: registering A2 and A3 as component types that page metadata can reference widens what metadata objectui accepts. The build round's amendment confirms or narrows this line.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12045,
    "status": "needs_decision",
    "branch": "claude/issue-12045-approval-detail-page",
    "pr": null,
    "session": "session_01CGZy1BGCjdN5cXqL9cnvB8",
    "premise_still_valid": true,
    "summary": "Measurement round, no code. Q1: RecordDetailView reads page metadata only from the metadata page type (usePageAssignment over meta.pages). It has no page prop, and the console has no client-side page registration. With no page, the request's own page synthesizes page:header, record:highlights, record:path, page:tabs (record:details, record:related_list on sys_approval_action) and record:discussion, and shows zero decision buttons (measured on the real view). An authored slotted page, the shape plugin-auth already ships for sys_user through its manifest pages, reached the same view. It rendered a newly registered type and record:quick_actions, whose Approve opened ActionParamDialog and POSTed to the request's approve route. Q2: in spec 17.7.0, PageComponentSchema.type is an open union, so safeParse admits any type that is not retired. The vocabulary closes at authoring time (lint rule component-type-unknown, inside the seven reserved namespaces), and props are checked only for ComponentPropsMap rows. Q3: ActionParamDialog already hosts a comment and several attachments; on main, the inbox drawer runs approve/reject through it with no exclude. It cannot show the timeline while it is open: it is modal, and its state carries only params, title and description. Two further findings: A2 needs no page node, because record_id is a declared referenceVia pointer pair that objectui never reads. And sys_approval_action is closed to ordinary positions on the data door, so the timeline needs A1 to route it to the actions route.",
    "tests": [
    "Spec probe (scratch node script against the installed @objectstack/spec 17.7.0 and @objectstack/lint 17.7.0, in a detached worktree at d99b731), exit 0. PageComponentSchema.safeParse results: record:related_list OK (enum control); user:profile FAIL custom@type (control for the retired-by-name refusal); OK for record:preview_card, record:approval_progress, record:approvals, record:attachments, approvals:record_preview, approvals:decision_progress and record-preview-card. PageSchema.safeParse of a slotted sys_approval_request page is OK for all three namings tried. validateComponentTypes: 0 findings for approvals:* and the kebab name; 2 component-type-unknown findings for record:preview_card and record:approval_progress; negative control record:detials gives 1. validateComponentProps: 0 findings for unknown keys on unregistered types; control record:details with key notAKey gives 1 component-props-unknown-key. slots.rightRail: FAIL unrecognized_keys.",
    "Real RecordDetailView probe (a scratch test in the console vitest project, mounted over createApprovalRequestsDataSource with only the HTTP transport doubled): bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c 'pnpm exec vitest run apps/console/src/services/zzprobe12045.recordPage.test.tsx' gave Test Files 1 passed, Tests 2 passed, and VERDICT command-exit 0 on the final run. S1 (no page metadata): the synthesized types are page:header, record:highlights, record:path, page:tabs, record:details, record:related_list and record:discussion; the tabs are Details and Related; there are 0 Approve and 0 Reject buttons. S2 (an authored slotted page): the registered approvals:record_preview rendered with its properties, and its RecordContext was sys_approval_request carrying viewer. record:quick_actions (location record_section) rendered Approve and Reject. Approve opened the param dialog with a Comment textarea and an Attachments file input (multiple=true). Confirm POSTed /api/v1/approvals/requests/req_12045/approve with body {comment: probe comment}.",
    "Existing pins on main, run read-only: ActionParamDialog.test.tsx, RecordDetailView.approvalDeclaredActions.test.tsx and tests/DeclaredActionsBar.test.tsx through the same lock gave Test Files 3 passed, Tests 100 passed, VERDICT command-exit 0. Among them: 'renders a file param as a real upload control, not a text box (#2698)' and 'carries the decision attachments param the hand-written buttons could not'.",
    "Two-leg grep for decision outputs (decision_output_defs / decisionOutputDefs): the control legs hit app-shell DeclaredActionsBar.tsx (4) and decisionOutputParams.ts (3); the test leg over plugin-detail, react and core gives 0 (exit 1). record:quick_actions therefore has no decision-output params.",
    "Cleanup: the scratch test is deleted (git status --porcelain: 0 lines; HEAD stays d99b731), node_modules is removed, and the worktree is removed (absent from git worktree list, directory absent, no claude/issue-12045 branch). The node probe is deleted from the scratchpad.",
    "NOT MEASURED: a live backend's data-door answer for an ordinary approver reading sys_approval_action. Reason: no backend was started this round. The closure is read from the object's own comment ('this table is deliberately closed to ordinary approver positions') and the spec's fileAccessDelegate doc."
    ],
    "mcp_calls": "0",
    "api_writes": "1 — POST /repos//issues/12045/comments (this os-dev-report), through scripts/pm/post-stamped.mjs and the fleet relay",
    "open_questions": [
    {
    "question": "Q1 — where does the sys_approval_request detail page's metadata live?",
    "options": [
    "A — objectui composes it. The drawer mounts RecordDetailView embedded over A1's get, with A2, A3 and DeclaredActionsBar as React siblings around the synthesized page (what the business record page does with the bar today). How far this is metadata: sections and the sys_approval_action related list come from object metadata, but the placement of A2, A3 and the bar is React. The synthesized page also brings record:path and a record:discussion thread, which only code can remove. No objectstack work, and it works on 17.7.0 backends. RecordDetailView takes no page prop (its props are dataSource, objects, onEdit, objectNameOverride, recordIdOverride and embedded), so any page shape beyond the synthesized one is code.",
    "B — a slotted page served from objectstack plugin-approvals: kind slotted, with alerts, highlights, tabs, and discussion set to an empty list. It is registered through the plugin's manifest pages, the way plugin-auth registers SysUserDetailPage from platform-objects. objectui only registers renderers and mounts RecordDetailView. How far this is metadata: fully. The placement is page metadata, and the probe shows objectstack's lint walks slots and page:tabs children. Measured: the real RecordDetailView picked an authored slotted page up through usePageAssignment and rendered it. Cost: an objectstack domain:services cross-repo child, with Blocked-by on this card. It lands on objectstack's v18 line (main is in changesets pre mode next, nothing is published under next, and 17.x ships no new release), so only v18 backends serve it. The slot menu has no aside slot (slots.rightRail is refused).",
    "C — found: A2 needs no page node. sys_approval_request.record_id is declared Field.text with referenceVia: 'object_name', the spec's polymorphic pointer pair. objectstack main declares 7 such fields on 6 objects: sys_approval_request, sys_activity (2), sys_audit_log, sys_record_share, sys_share_link and sys_automation_run. objectui reads referenceVia nowhere at runtime (only two comments in plugin-list), and getCellRenderer dispatches on the field type alone. If A2 becomes the display renderer for a referenceVia field, record:details draws the card wherever the pair shows, B2's target column gets it for free, and so do the other five objects. C composes with A or B for A3 and the actions."
    ],
    "recommendation": "B + C. Business need: every approver and submitter who opens a request; C also serves the 6 objects that measurably declare pointer pairs. Long-term soundness: B is the ruling's stated reason (5166015115: AI edits spec-validated metadata, not ungated React); A keeps the placement in React, the very drift this epic exists to cure. C implements a key the spec already declares (spec declaration above implementation) and adds no surface. Hard for AI to get wrong: B's page goes through os validate and lint (the type vocabulary, plus the props gate for declared types); A has no validator; C adds no authorable key at all. Startup scope: C adds zero authorable surface; B adds one page and at most one new type (Q2). The trade-off for the maintainer: B moves B1's landing to v18 backends, while A lands on 17.7.0 now. ⛔ A now and B later is not recommended: a staged transition needs named external-user evidence, and none was found."
    },
    {
    "question": "Q2 — can page metadata reference a newly registered objectui component type under @objectstack/spec 17.7.0, and how should B1 name its new components?",
    "options": [
    "Measured basis. PageComponentSchema (spec ui/page.zod) declares type: z.union([PageComponentType, z.string()]) followed by a .superRefine that refuses only RETIRED_PAGE_COMPONENT_TYPES, so the parse is open and safeParse admits any type that is not retired. The closure is at authoring time: component-type-vocabulary.ts derives RESERVED_COMPONENT_TYPE_NAMESPACES (page, record, app, nav, global, ai, element) from the enum, and @objectstack/lint's component-type-unknown rule refuses an unknown type inside them. validateComponentProps checks props only for types with a ComponentPropsMap row. At runtime, objectui rendered a newly registered type from page metadata (probe S2).",
    "A — own namespace (for example approvals:decision_panel), with no spec change. It parses and lints clean on 17.7.0 today. Its props are unchecked: measured 0 props findings for an unknown key on an unregistered type, against 1 for the record:details control. A misspelled prop is silently ignored.",
    "B — a spec v18 ComponentPropsMap row, in the record namespace or its own, so os validate checks the props bag. This is a spec-lane card. objectui's tests parse it only after objectui#12030 (held until next publishes). A page from Q1-B ships on v18 anyway, so the timing aligns.",
    "C — narrow first. A2 needs no type (Q1-C). A3 and the decision actions fold into ONE type: the drawer shows them together today, and RecordApprovalsPanel already composes progress with the bar for business records. That is one new type, not three."
    ],
    "recommendation": "C + B: one new type with a v18 spec row. Business need: the decision panel is the one component the request page lacks. Long-term soundness: contract-first, with the row declaring what the renderer reads. Hard for AI to get wrong: declaration means enforcement, and the props gate catches typos; A leaves a silent-ignore props bag, the consumer-side tolerance this frame warns against. Startup scope: one row instead of three. A only if the maintainer rejects v18 timing, and then with that cost stated. Related, carried as a finding: objectui's existing record:approvals and record:attachments already sit in the reserved record namespace with no row, so an authored node of either is refused by component-type-unknown."
    },
    {
    "question": "Q3 — can ActionParamDialog host the approval composer (a comment, several attachments, the timeline's context), or does B1 need one thin custom approve/reject action?",
    "options": [
    "A — ActionParamDialog through DeclaredActionsBar, inside the Q2 decision panel, with no custom composer. Proven: plugin-approvals declares approve/reject params as comment (textarea) and attachments (file, multiple). The probe opened the dialog with both and POSTed to the request's route with {id} resolved to the request. 100 of 100 existing pins pass. The inbox drawer on main already does exactly this: it mounts DeclaredActionsBar with no exclude, so the 'richer inline composer, excluded from the bar' in the parent's caveat is gone from main. Limitation: ParamDialogState carries only params, title and description, and the dialog is modal, so the timeline can be read before and after composing but not during.",
    "B — the spec's existing record:quick_actions (no new type for the actions). Measured: it rendered approve/reject from wire-shaped CEL envelopes and posted correctly. But decision-output params (decision_output_defs becoming outputs.KEY params), the override notice naming bypassed approvers, and approver-name resolution exist only in DeclaredActionsBar (two-leg grep). For an approval node that declares decision outputs they are silently dropped.",
    "C — one thin custom approve/reject action: a non-modal inline composer (comment, several files, with the timeline beside it), registered as a component-level exception, with approve/reject excluded from the bar. It re-implements upload staging and decision outputs."
    ],
    "recommendation": "A. Business need: the live approver surface (the drawer) already decides through this dialog, and no pull for composing beside the timeline was measured once the inline composer left. Long-term soundness: one decision path shared by the drawer, the business record page (objectui#3055) and B1. Hard for AI to get wrong: the decision params stay declared metadata in plugin-approvals; C moves the form back into React, and B silently drops outputs. Startup scope: no new composer. C only if the maintainer names a need to compose beside the timeline."
    },
    {
    "question": "Q4 — the drawer's thread reply (POST /approvals/requests/ID/comment, stored as a sys_approval_action row with action comment) has no declared action. What does B1 do with it?",
    "options": [
    "A — declare approval_comment on sys_approval_request in plugin-approvals: comment required, attachments optional, visible for the submitter or a pending approver, matching the service's own check. The decision panel then carries it through the same dialog. This is objectstack work in the Q1-B child.",
    "B — B1's timeline is read-only, and the reply stays in the bespoke drawer until B3 decides.",
    "C — a bespoke reply box inside the decision panel."
    ],
    "recommendation": "A. Business need: the reply is the one drawer interaction that does not ship as metadata. Long-term soundness: the same declared-action path as the nine others. Hard for AI to get wrong: a declared param set, not React. Startup scope: one action declaration in the child Q1-B already needs, and no new objectui surface."
    }
    ],
    "out_of_scope_findings": [
    "carrier: B3 (it deletes the drawer) · observation, no class: ApprovalsInboxPage still carries the comment saying the rich approve/reject composer stays because the generic param dialog cannot yet collect file attachments, while the drawer on main mounts DeclaredActionsBar with no exclude and runs approve/reject through ActionParamDialog · noted, not filed",
    "carrier: B1's build round, where Q2 settles naming · objectui registers record:approvals and record:attachments inside the spec-reserved record namespace with no ComponentPropsMap row, and STRING_ARM_REGISTERED_TYPES is empty. @objectstack/lint 17.7.0 validateComponentTypes therefore refuses an authored node of either, while RecordDetailView honours an authored record:approvals (hasExplicitApprovals). Measured on the lint function, not through the os validate CLI. objectstack main has no authored producer (zero hits), so no reach is established · dedupe words: record approvals reserved namespace component-type-unknown vocabulary · noted, not filed",
    "carrier: 承接者:无 · buildDefaultPageSchema reads slots.rightRail, which the spec's slot map refuses (unrecognized_keys, measured), so no served page can reach it; dormant · noted, not filed",
    "carrier: B1's build round · on the request's own page, RecordDetailView's useRecordApprovals still reads /approvals/requests?object=sys_approval_request&recordId=ID (approvals on the request itself, always empty). That is one wasted read per page open, seen in the probe's request log · noted, not filed"
    ],
    "build_round_surface": {
    "objectui": [
    "Mount: apps/console/src/pages/system/ApprovalsInboxPage.tsx. The ?request= Sheet hosts RecordDetailView embedded over A1's get; B3 removes the bespoke drawer body.",
    "Data source: apps/console/src/services/approvalRequestsDataSource.ts. Route sys_approval_action reads scoped to one request_id to GET /approvals/requests/ID/actions; the data door is closed to ordinary positions. Also put the routed source behind every sys_approval_request record page. The Setup Requests object route reads through the data door today, which attaches no viewer or decision_progress (attachViewers runs in listRequests and getRequest only). A1 is not yet wired anywhere outside its tests.",
    "A2 via referenceVia: packages/fields/src/index.tsx (getCellRenderer dispatches on type only; it needs a referenceVia branch), plus app-shell registering packages/app-shell/src/views/record-preview/RecordPreviewCard.tsx as that renderer. Whether plugin-detail's details grid goes through getCellRenderer is to be measured in the build round.",
    "Decision panel type: a new renderer under packages/app-shell/src/views/ that composes DecisionProgressIndicator (views/approval-progress) and DeclaredActionsBar, registered through ComponentRegistry.register (the views/record-approvals-renderer.tsx pattern) and imported for its side effect from packages/app-shell/src/index.ts. Add a packages/types node type if it is authorable (AGENTS #0.1). packages/cli/src/utils/known-schema-types.ts lists record:approvals today and would list the new type.",
    "Tests: the real RecordDetailView over A1 with a served slotted-page fixture (this probe's shape), referenceVia renderer pins, routed sys_approval_action pins, and a changeset (the claim's Clause-② yes: minor on the package whose registered surface widens)."
    ],
    "outside_objectui": [
    "objectstack plugin-approvals (domain:services, a cross-repo child with Blocked-by on this card): a slotted sys_approval_request page file, the manifest pages entry, its pages.NAME.label translations as platform-objects carries them, and approval_comment if Q4-A.",
    "objectstack @objectstack/spec v18: a ComponentPropsMap row for the decision panel (Q2-B), and a vocabulary answer for objectui's existing record:approvals and record:attachments.",
    "objectui#12030 (move to next) before objectui's tests can parse v18 spec rows."
    ]
    },
    "gates": [
    "none owed: measurement round with no diff; the dispatch says no gate is owed. The probe runs are listed under tests."
    ],
    "deviations": [
    "Worktree rule 1: the probe worktree was created detached (git worktree add --detach, off origin/main d99b731), with no local branch and no empty-branch push. The dispatch's write budget is one comment and nothing else, and the claim says no file changes on the branch.",
    "Q2's 'name the schema and the line': named by symbol and quoted line content, not by line address, because objectui AGENTS §5 #11 bans line addresses in issue comments.",
    "The scratch probe test lived inside the probe worktree (apps/console/src/services/), because the console vitest project collects only inside the repo. It was never committed, then deleted, and the worktree was removed. The node probe lived in the scratchpad and was deleted.",
    "The first RecordDetailView probe run used bare-string visible predicates, and record:quick_actions hid approve/reject ('has is not a function' on the legacy evaluator). That fixture was unfaithful: ObjectSchema.create parses a bare string into the {dialect: cel} envelope, so the wire carries the envelope. The rerun with the wire shape rendered both. Recorded so the first reading is not mistaken for a finding.",
    "Added a build_round_surface field. The dispatch asks for the build round's touch list with paths, and no standard report field carries it."
    ],
    "files_changed": [],
    "line_budget": "not applicable (no skills/** surface)"
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Decision request — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T10:01Z. State: pm:dispatched → needs-user-decision in this act. The claim, the assignee and the branch stay. No code has been written: the dev's measurement round is report 6078632273.

    一句话问题

    审批请求详情页(审批人打开一条请求看到的页面)有两种做法:

    • 现在就在 console 里用 React 拼出来,17.7.0 的后端立刻能用;
    • 写成平台的页面元数据(放在 objectstack 的审批插件里),完全受校验,但只能在 v18 后端上用。

    另外,如果走元数据,新的"审批决策面板"组件要不要在 v18 的 spec 里加一行属性声明,这会碰到你 08-03 裁决里"非协议变更"那句话。

    背景

    • 审批 epic objectui#2763 的 A1、A2、A3 都已落地:
      • 审批数据源 d99b731;
      • 记录预览卡片 049012b;
      • 进度组件 ba9e820。
    • B1 要把它们和动作时间线、DeclaredActionsBar 组成请求详情页。
    • dev 的测量轮(6078632273,只读,不改代码)实测了三件事:
      • RecordDetailView 只从元数据的 page 类型读页面,没有 page 参数,console 也没有客户端页面注册。不写页面时,它自动合成"详情 / 相关"两个页签,决策按钮为 0。
      • 用一份带槽位的页面(形状同 plugin-auth 给 sys_user 发的那份)时,同一个视图渲染出了新注册的组件类型和 record:quick_actions。点"同意"会打开 ActionParamDialog,带评论框和多附件,并 POST 到请求的 approve 路由。
      • spec 17.7.0 的 PageComponentSchema.type 是开放的联合类型,只拒绝已退役的类型。词汇在作者侧由 lint(component-type-unknown,只管 7 个保留命名空间)收口。属性只对 ComponentPropsMap 有行的类型做校验。
    • objectstack 的 main 已进入预发布模式(tag next),17.x 不再发版(你今天的原话:「17.x 不会发新版」)。所以 plugin-approvals 里新加的任何页面都只随 v18 发布。objectui 改用 next 已立卡 objectui#12030,等 next 发出。

    Governing text

    Governing text: maintainer ruling 5166015115 (objectui#2763, 2026-08-03), quoted verbatim:

    • 「裁决理由记录:重建后 AI 改审批面改的是受 spec 校验的元数据,而非无门禁的定制 React」
    • 「排期:v17 后按 epic 走(非协议变更)」
    • 「composer 富交互按本单 honest caveat 处理:先证明 ActionParamDialog 能承载(评论 + 多附件 + 时间线上下文),不能则保留一个薄的定制 approve/reject action」

    冲突所在: 第一条指向"做成元数据",第二条写着"非协议变更"。元数据路线要么接受属性不受校验(违背第一条的"受 spec 校验"),要么在 v18 的 spec 里加一行(是协议增项,碰第二条)。本席不能在两句裁决之间替你取舍,所以回箱。

    本席已按裁决自行定下、不在此请示的一项: 审批意见框走 ActionParamDialog,不做定制意见框。裁决的条件是"先证明能承载",dev 实测已证明:评论 + 多附件可以,main 上的审批抽屉现在也这样用,现有 100 条测试全过。唯一做不到的是"打开对话框时同时看时间线",因为对话框是模态的。

    前提(每条带复查命令)

    1. plugin-approvals 里没有 sys_approval_request 的页面。 git -C /home/user/objectstack grep -n -E "^\s*pages\s*:" origin/main -- packages/plugins/plugin-approvals/ 应为 0 行;对照组 plugin-auth 同一命令命中 1 行(auth-plugin.ts,pages: [SysOrganizationDetailPage, SysUserDetailPage])。
    2. 页面组件类型在解析时开放、在 lint 时收口。 git -C /home/user/objectstack grep -n "RETIRED_PAGE_COMPONENT_TYPES\|z.union(\[PageComponentType" origin/main -- packages/spec/src/ui/。
    3. next 尚未发布。 npm view @objectstack/spec dist-tags:只有 latest 17.7.0 和 rc,没有 next。
    4. RecordDetailView 没有 page 参数。 git -C /home/user/objectui grep -n "export function RecordDetailView(" origin/main -- packages/app-shell/src/views/RecordDetailView.tsx:参数只有 dataSource, objects, onEdit, objectNameOverride, recordIdOverride, embedded。

    选项 × 真实代价

    选项 做什么 客户可感知的后果
    甲 现在用 React 拼 抽屉里嵌入 RecordDetailView(走 A1 数据源),A2、A3 和决策按钮作为 React 兄弟节点摆在合成页四周。不碰 objectstack,不改 spec。 审批人在现有 17.7.0 后端上立刻用上标准页面。但页面的布局在 React 里,合成页自带的"路径"条和讨论区只能写代码去掉,以后改布局还是改代码。
    乙 元数据 + v18 spec 声明(dev 推荐) 在 plugin-approvals 写一份带槽位的请求页面(经 manifest pages 注册,同 plugin-auth 的做法)。objectui 只注册渲染器。新增一个"审批决策面板"组件类型(进度 + 决策按钮合一),在 v18 spec 里加一行 ComponentPropsMap,os validate 能校验它的属性。 页面完全由元数据决定,改布局只改元数据,有校验兜底。但只有 v18 后端能用:要等 next 发布、objectui#12030 切过去,现有 17.x 用户看不到。新增一行 spec 是协议增项。
    丙 元数据,但不加 spec 声明 同乙,但新组件类型用自己的命名空间(如 approvals:decision_panel),17.7.0 下解析和 lint 都通过。 时间点同乙(页面在 plugin-approvals,照样只随 v18 发布)。属性不受校验:作者把属性名拼错,会被静默忽略。

    两条与选项无关、本席会一并执行的:

    • dev 发现 sys_approval_request.record_id 已在 spec 里声明为多态指针(referenceVia: 'object_name'),objectstack 有 6 个对象共 7 个这类字段,但 objectui 运行时从来不读它。把 A2 卡片接成这类字段的显示渲染器,就是补实现一个已声明的键,不新增任何可写的面。甲、乙、丙都需要它。
    • 时间线上的"回复"(POST /approvals/requests/ID/comment)目前没有声明式动作。走乙或丙时,在 plugin-approvals 声明一个 approval_comment 动作;走甲时,时间线只读,回复留在旧抽屉里,等 B3。

    业务含义直译:

    • 甲是"先用临时隔断把房间分出来,马上能住,以后改格局要拆墙"。
    • 乙是"按图纸施工,验收合格才交房,但要等新楼盘(v18)开盘"。
    • 丙是"按图纸施工、不做验收"。

    四轴(业务立场)

    • 项目长远合理性: 你的裁决理由就是"AI 改的是受 spec 校验的元数据"。
      • 乙完全兑现:布局在元数据里,属性有校验。
      • 丙兑现了一半:布局是元数据,属性没有校验。
      • 甲把布局留在 React 里,正是这个 epic 要根治的偏离。
      • 两年后,审批面应当和 Salesforce Lightning 的 Record Page 一样,是一份可校验的页面定义,而不是一页手写代码。
    • 实际业务拉动: 审批人今天用的旧抽屉能正常审批(实测走的就是 ActionParamDialog),没有测到"必须马上换"的拉动。甲的好处是早;乙、丙要等 v18,对审批人来说是晚,而不是用不了。
    • 防 AI 犯错:
      • 乙下,AI 写错页面里的组件属性,os validate 当场报错。
      • 丙下,写错的属性会被静默忽略,页面少一块,没人报错。
      • 甲下,布局改动走 React 代码审查,没有元数据校验。
    • 创业阶段不扩散: 乙只加一行 spec(一个组件类型),丙不加 spec,甲不加任何东西。三者都只新增一个组件。

    推荐

    推荐:乙。 只看①选乙;②③④ 是否翻转:否。② 只影响时间点:现有抽屉能用,没有测到紧迫拉动。③ 同向。④ 只多一行 spec。

    回退:丙。 如果你坚持"非协议变更",就选丙:页面仍是元数据,代价是属性不受校验,注释里要写明这一点,等以后再补声明。

    不推荐:甲。 "先甲后乙"是分期过渡,按创业阶段"过渡从紧"的原则,需要有具名外部用户的证据才荐。dev 没找到这样的证据。

    置信缺口:

    • 没有起真实后端去测"普通审批人读 sys_approval_action"的回答,目前只读到对象的注释说它对普通岗位关闭。所以 A1 要把时间线路由到 actions 接口,这一点还没实测。
    • v18 的发布时间不在本席视野内:objectstack#22085 和 #22130 仍在飞,之后还要你批准发布。

    裁后执行

    • 乙或丙:
      1. 本席在 objectstack 立一张 domain:services 子卡:plugin-approvals 的请求页面、manifest pages 条目、翻译,以及 approval_comment 动作。本卡挂 Blocked-by: 那张子卡。
      2. 选乙时,再立一张 spec 车道卡:v18 的 ComponentPropsMap 行。
      3. objectui 这边的 B1 构建轮先做不依赖 v18 的部分:A2 接成多态指针字段的渲染器、决策面板组件、A1 数据源接上请求页与时间线路由。页面挂载等 objectui#12030 切到 next 之后再做。
    • 甲: 本席修订认领,dev 直接在 console 抽屉里组装,17.7.0 下落地,Clause-② 改为 no。

    相关:

    • 父卡 objectui#2763,裁决 5166015115;
    • A1 objectui#12032,A2 objectui#12029,A3 objectui#12033;
    • objectui#12030(改用 next);
    • objectstack#22050(v18 开工的裁决 6037890422);
    • objectstack#22085、#22130(next 发布的前置);
    • dev 测量报告 6078632273。

    os-decision-facets

    • ① 项目长远合理性:乙完全兑现"受 spec 校验的元数据";丙只做到布局元数据化,属性不受校验;甲把布局留在 React,正是这个 epic 要根治的偏离。
    • ② 实际业务拉动:现有抽屉能正常审批,没有测到紧迫拉动;甲早,乙、丙等 v18。
    • ③ 防 AI 犯错:乙下写错属性 os validate 当场报错;丙下静默忽略;甲没有元数据校验。
    • ④ 创业阶段不扩散:三者都只新增一个组件;乙多一行 spec,丙、甲不改 spec。

    Prior rulings read: approvals metadata / page / composer / v18 timing → 4 hits (5166015115, 6006187681, objectstack#22050 6037890422, today's 「让 objectui 提前改用 next 预发布版」 on objectui#12030); ADR none on approval pages; thread: 2
    推荐:乙。只看①选乙;②③④ 是否翻转:否。
    置信缺口:普通审批人读 sys_approval_action 的真实回答未实测;v18 发布时间不在本席视野内。


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #303 item 2 · letter 乙 (a metadata page in plugin-approvals, one v18 spec ComponentPropsMap row) · maintainer 「同意」 2026-10-09T11:17Z

    Director seat, summon #35, session_01VYToj6PQehTEKNrjGM9akg (GitHub os-zhuang; written as objectstack-fleet[bot] via the relay). Presented in batch #303 from the domain:ui seat 3's decision request (6078697273) on the dev's measurement round (6078632273), B1 of the approvals epic objectui#2763: 甲 objectui composes the page in React now, on 17.7.0 backends; 乙 a slotted page authored in objectstack's plugin-approvals (manifest pages, as plugin-auth ships sys_user's), objectui registering renderers only, one new "decision panel" component type declared in v18's ComponentPropsMap so os validate checks its props; 丙 the same page with the type in its own namespace and no spec declaration. The seat recommended 乙 with 丙 as the fallback; this seat recommended 乙; the maintainer answered 「同意」. Thread-read: 6078697273. Freshness: body unchanged; no comment since the presentation; labels enhancement, needs-user-decision, priority:p3, pm:epic, domain:ui, area:workflow; the claim, assignee and branch kept, as the request said. Premises re-read: objectstack origin/main ships no pages: in plugin-approvals (0 hits; plugin-auth, the control, 1), PageComponentSchema.type is an open union refusing only RETIRED_PAGE_COMPONENT_TYPES, and props are checked only for ComponentPropsMap rows; objectui main's RecordDetailView takes no page prop; objectstack main is in changesets pre mode next, nothing is published under next, and 17.x ships no new release (the maintainer today: 「17.x 不会发新版」).

    The ruling

    乙 — the page is metadata in plugin-approvals, and the decision panel is declared on v18's spec. The request page is a slotted page in objectstack's plugin-approvals, registered through the manifest's pages; objectui registers the renderers and mounts RecordDetailView; one new component type, the decision panel (A3's progress and the decision actions in one), is declared in v18's ComponentPropsMap, so its props are validated (Clause-②: yes, a v18 widening). The 2026-08-03 ruling (5166015115) is read as follows, settled here: its first sentence governs (AI edits spec-validated metadata, never ungated React); its scheduling note "(非协议变更)" was written while 17.x still shipped, and with main on the v18 pre-release line and 17.x closed, one ComponentPropsMap row on v18 is an ordinary widening of that line, not a protocol door opened for 17.x. Carried regardless of the letter, as the seat proposed: A2 becomes the display renderer for referenceVia pointer fields (a key the spec declares on 7 fields of 6 objects and objectui never read); the timeline's reply is a declared approval_comment action; the composer stays ActionParamDialog, proven to carry a comment and several attachments, and the modal's inability to show the timeline beside it is accepted. ⛔ Not taken: 甲 (the placement stays in React, the drift this epic cures; a 甲-then-乙 transition would need named external-user evidence, and none was found), 丙 (unchecked props: a misspelled property silently drops a block).

    Execution order, as the request's 裁后执行 states: the ui seat files an objectstack domain:services child (the request page, the manifest pages entry, translations, the approval_comment action) and a spec-lane card (the v18 ComponentPropsMap row); this card carries Blocked-by: on both for the page mount; objectui's build round does the v18-independent parts first (A2 as the pointer renderer, the decision-panel component, A1 wired to the request page and the timeline routed to the actions route); the page mounts after objectui#12030 switches objectui to next.

    Prior rulings read: 5166015115 (the parent's ruling, its scheduling note read as above); 6006187681; objectstack#22050 ruling 6037890422 (the v18 start); objectui#12030 (the switch to next, on the maintainer's 「让 objectui 提前改用 next 预发布版」); the A children's records 6076152376, 6077174661, 6077915878. 自检: 只看①选乙;②③④ 是否翻转:否(② 只影响时间点:现有抽屉今天能正常审批)。置信缺口:普通审批人读 sys_approval_action 的真实回答未起后端实测(A1 需把时间线路由到 actions 接口);v18 的发布时间不在本席视野内(objectstack#22085、#22130 在飞,之后要维护者批准发布)。

    State

    • needs-user-decision → pm:dispatched in this act (the claim 6078298531, the assignee and the branch claude/issue-12045-approval-detail-page stay, as the request kept them); the Ruled: line added to the body. The ui seat files the two child cards and writes the Blocked-by: lines, then runs the v18-independent build round.

    Generated by Claude Code

  6. 18 remaining items

  7. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (B1's page-mount step, the fresh claim that ACCEPT 6082369712 and the unlock scan 6159947599 name)
    Session: session_01JZnppgHRp4sREcouhkQYDB
    Account: sunhaolin
    Branch: claude/issue-12045-page-mount
    Worktree: objectui-issue-12045
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: apps/console/src/AppContent.tsx (the request record routes, and the export comment that names a test file main does not have), apps/console/src/services/approvalRequestsDataSource.ts and its tests, apps/console/src/pages/system/ApprovalsInboxPage.tsx (where a request opens) and its tests, packages/app-shell/src/views/RecordDetailView.tsx, ObjectDataPage and InterfaceListPage only where a request record surface picks its data source, .changeset/12045-approval-decision-panel.md (append-only, for the console bullet nit), and one new .changeset/12045-*.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui names no path-derived mandate)
    Clause-②: no
    Responsibility: n/a — not a defect card
    Thread-read: 6159947599
    Serial constraints cleared: read at objectui main fc528c3eb.

    • Already done, not in this round: the registration of record:approval_decision, its index import, its known-types line and its zod arm landed with objectui#12342 (PR feat(app-shell): register record:approval_decision, armed in @object-ui/types (objectui#12342) #12362, 52d6194).

    • Out of this surface: packages/app-shell/src/views/ObjectView.tsx (seat 2's objectui#12417 and seat 3's objectui#12423 are in flight in it) and packages/i18n/** (objectui#12423). If the drawer or split-overlay route needs either, the dev stops and reports.

    • No other overlap: no open PR names these files. objectui#12077 (pending approvers' names) is pm:blocked on this card and stays out of this round.

    • Accept-set reading: console routing and the place a request opens. No @object-ui/* export, prop or option changes. If one is needed, the dev stops and reports, and Clause-② is re-stated.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12045,
    "status": "needs_decision",
    "branch": "claude/issue-12045-page-mount",
    "pr": "#12432",
    "session": "session_01JZnppgHRp4sREcouhkQYDB",
    "premise_still_valid": true,
    "summary": "Measured A3-A5 first, then built A4 and A6. The inbox mount was not built: A5's gap table shows losses, so it is the open question below. A3: on a real RecordDetailView, the served sys_approval_request_detail draws end to end: the decision panel (tally, Approve, Reject, Reply), the highlights strip without record_id, and the Details and Timeline tabs, with the timeline read from the actions route. Three gaps on an unmutated run under the build round's route: (1) the Timeline tab's count probe sent $count and was refused; (2) RelatedList's label read of request_id was refused by the scoped list ({$filter:{id:{$in:[id]}}}), which falsifies the carried pin's premise that this read is not refused on an unmutated run; (3) the Details tab renders empty: a bare record:details draws nothing in objectui, so the A2 record card and every field outside the strip are drawn nowhere. (3) is off this surface and reported below. A4: every surface that opens a request reads useAdapter() at or below DefaultAppContent. So the console's AppContent now wraps DefaultAppContent in ApprovalRecordReads, which re-provides AdapterCtx (an existing @object-ui/react export) as routeApprovalRecordReads(adapter): a request's findOne goes to the approvals get, one request's timeline to the actions route, and lists of requests and everything else stay on the data API. The routed adapter is cached in a WeakMap keyed on the adapter. That one layer covers the record route, ObjectView drawers and split overlays (reachable only when a view authors navigation; with none, the field count derives page), ObjectDataPage and InterfaceListPage. The no-app branch has no record route at all. The console's own request route (approvalRequestRoutes) retires. A timeline read honours $count. A6: the label read now reaches the data API and is pinned not refused; the export-comment nit is gone with the retired export; the changeset console-bullet nit is fixed append-only. The record:approval_decision registration was confirmed landed (PR #12362) and not redone. No @object-ui export, prop or option changed (Clause-2 no holds); console patch only.",
    "tests": "Union at head b688d16: pnpm exec vitest run apps/console/src/services/ apps/console/src/pages/system/ApprovalsInboxPage apps/console/src/tests/AppContent → 'Test Files 23 passed (23)', 'Tests 180 passed (180)', lock VERDICT command-exit 0. New pins: approvalRequestsDataSource.servedPage.test.tsx (the served page whole, through the generic route under ApprovalRecordReads: the panel tally and decisions from the approvals get; the Timeline count '2' and rows from the actions route; the request_id label read reaching the adapter with no 'Failed to resolve lookups' warning; provider identity across renders and remounts; routing; a no-adapter control), approvalRequestsDataSource.recordReads.test.ts, AppContent.approvalRecordReads.test.tsx (the real AppContent hands a DefaultAppContent probe the routed adapter), a $count case in the timeline test, and the decisionPage test re-pointed to the layer with a no-provider control. Ablations: predictions were written first; each leg ran through objectstack scripts/ablation-replace.mjs (anchor 1 → 0, blob changed, restore blob == HEAD, git diff HEAD empty). Console tests import src, so there is no dist leg. AB1 ($count dropped): 2 red / 17 green, as predicted. AB2 (findOne to host): 14 red / 12 green; predicted red, but wider than listed, because every page-level test needs the record read; controls green. AB2's first attempt was refused by the tool (the replacement was a substring of the anchor): no mutation, no measurement, rerun with a distinct replacement. AB3 (the layer refuses request lists, the build-round behaviour): 4 red / 9 green, as predicted. AB4 (WeakMap set dropped): 1 red / 8 green, as predicted. AB5 (AppContent without the wrapper): 1 red, control green, as predicted. A3's readings came from a scratch probe test inside the worktree, never committed and deleted.",
    "gates": "Local subset (exit codes captured to files before any pipe). Console dependency closure built: turbo run build --filter=@object-ui/console^... --concurrency=2, VERDICT command-exit 0. pnpm --filter @object-ui/console type-check: exit 0 at b688d16 (script name echoed). eslint on the 7 touched TS files: exit 0, 0 errors, 1 warning (the pre-existing systemRoutes react-refresh warning; base had 2, one of them the retired approvalRequestRoutes). Exit 0: check:control-bytes, check:new-line-citations ('0 new citation(s)'), check-changeset-no-major, check-changeset-presence, check:pending-changeset-literals, check:i18n-keys, check:test-path-roots, check:vi-mock-specifiers / vi-mock-inherit / vi-mock-override-shape. check-changeset-overwrite: exit 0, report-only, names the appended 12045-approval-decision-panel.md. check:changeset-claims: exit 0, report-only, names 12032-approval-requests-data-source.md (see findings). check:eager-closure: exit 0, 'Console eager closure is 3075.7 KB gzipped across 69 of 2252 chunks (budget: 3119.4 KB, headroom: 43.7 KB)'. check:sdui-registration-pins: exit 0, 'All 16 registration(s) a sideEffects array promises are present in the built console'. Both bundle gates were read on the vite build of bce9baf; b688d16 changes one test file only. check-governed-queue-guard --test on the diff's paths: NOT GOVERNED. Not applicable: check:side-effects-array and the RecordDetailView suites (app-shell is unchanged). No --ran reconciliation: dispatch-gates refuses a sister repo, so the list was derived by hand from objectui package.json and the workflows. CI on PR 12432 head b688d16: 36 check-runs, 5 success, 3 skipped, 28 in_progress at report time (not awaited, per contract).",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes plus git push. (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot] as POST /repos/objectstack-ai/objectui/pulls (draft) and POST /repos//issues/12432/assignees (sunhaolin); read back 15035 of 15035 bytes identical, assignees sunhaolin. (2) This os-dev-report comment through scripts/pm/post-stamped.mjs: POST /repos//issues/12045/comments. Reads were REST GETs (issue, comments, one comment, the PR, check-runs). No label write.",
    "open_questions": [
    {
    "question": "B1's inbox mount: A5's gap table shows the served page loses faces the inbox sheet's tests pin. Lost: the Waiting-on approver chips with position labels and 会签 groups (approverHeadline-12110, characterizationPins); the declared-amount lead and the payload summary with declared labels, faces and the hidden trim (summaryFaces-11677, hiddenFieldTrim); the Flow-initiated origin; the raw-payload panel for studio.access (rawPayloadGate); the record link withheld or kept, the tombstone and the unresolvable affordance (recordLinkRefused-11878, tombstone, unresolvableReference: the served page draws no record reference at all, because its Details tab is empty); the vertical step stepper (stepProgressVertical). Rendered: the tally, the declared decisions, the activity (as the Timeline tab) and the reply (as approval_comment). What should a request open on from the inbox?",
    "options": [
    "A, link and keep the sheet. The sheet keeps every face and gains one 'Open as full page' link to the request's record route, which now reads through the routed layer. The copy may reuse console.objectView.expandToPage, present in the en pack; whether the i18n gates accept a cross-namespace reuse is NOT MEASURED, and a new key would be packages/i18n work, where objectui#12423 is in flight. Cost: one small console change plus a pin, about half a round. Business need: no pinned fix regresses, and the page is one click away. Long-term: keeps the hand-built sheet, the drift this epic cures, and it may only shrink (ADR-0060 rule 3). AI-proofing: neutral, the sheet stays React. Startup: smallest, adds no spec surface.",
    "B, the page replaces the sheet now. Cost: medium. Business need: regresses the real reported fixes the table names (raw-payload gate, tombstones, summary faces, 6-step CJK stepper, position labels) and deletes their pins. Long-term: the right direction, a regressive step. AI-proofing: the placement becomes metadata. Startup: no new surface, but it takes faces away from users.",
    "C, the page gains the lost faces upstream, then replaces the sheet. That needs: the empty Details tab answered (the renderer's declared fallback, or the page authoring sections); generic blocks or spec rows for a payload summary with declared faces and the hidden trim, approver chips with position labels and groups (which overlaps objectui#12077), a step stepper, a record reference with tombstone and unresolvable states, and a capability-gated raw panel. Each is a contract review across objectstack spec, plugin-approvals and objectui. Cost: several rounds and several cards. Business need: same faces, metadata-driven; no measured demand for these blocks beyond this page. Long-term: the north star of ruling 6079807016. AI-proofing: best, every face spec-validated. Startup: widens the spec, and new blocks need pull evidence."
    ],
    "recommendation": "A now. B is not taken. C is carded face by face only when the maintainer wants the sheet retired. Business need: A regresses none of the pinned, reported fixes, and B regresses many. Long-term: A keeps C open, and the sheet may only shrink. AI-proofing: no option makes today's sheet worse, and C is the eventual cure. Startup focus: A adds no spec surface, while C's new blocks have no pull outside this page yet. Conflict to weigh: the parent's scope says B1 mounts the page where the drawer is today, and A only links to it."
    }
    ],
    "out_of_scope_findings": [
    "class: b · reach: named producer: objectstack plugin-approvals sys_approval_request_detail (its Details tab is a bare record:details), measured on a real RecordDetailView: the record:details node renders as one empty space-y-6 div · evidence: in objectui, record:details with no sections and no fields renders an empty body. @objectstack/spec RecordDetailsProps' retired-layout text says omitting sections 'falls back to the object's highlightFields'; objectui's own renderer comment says the same; the page's comment expects 'the request's own fields by the object's field groups'. The renderer does neither, so on the served page the A2 record_id card and every request field outside the strip are drawn nowhere. If the answer is field groups, the State group carries payload_json and node_config_json, which must not reach a business approver as raw text (the inbox's raw-payload gate). Seam: spec:RecordDetailsProps.sections (omitted) → renderer:RecordDetailsRenderer (plugin-detail record-details) | producer: plugin-approvals sys-approval-request.page.ts · dedupe words: record:details empty body no sections; record details highlightFields fallback; served approval request page details tab empty",
    "carrier: domain:ui seat 1 (this claim's seat) · .changeset/12032-approval-requests-data-source.md's 'Superseded in part' paragraph says the console mounts the routed source on the request record route; this PR retires that route, so the paragraph is partly stale when both release together (check:changeset-claims names the file). It is outside this claim's file surface, so it is not edited; an append-only line after the PR 10823 precedent would close it · noted, not filed",
    "carrier: objectui#12077 · bearing: the served page draws pending_approvers nowhere (not in highlights, and the Details tab is empty), so the raw-id face does not arise on the page; the inbox sheet's chips remain the one approver-name face · noted, not filed"
    ],
    "deviations": [
    "One test file beyond the listed surface: apps/console/src/tests/AppContent.approvalRecordReads.test.tsx, the wiring pin for AppContent.tsx (ablation AB5 shows it is the only pin on that wiring).",
    "The console's own request record route (approvalRequestRoutes) was retired rather than extended: the layer makes it redundant, and keeping it would keep the scoped list that refused the label read. This is within the claimed file, with no export of the package entry; AppContent's exports are not in the published plugin entry.",
    "The decisionPage test's source moved from the scoped source to routeApprovalRecordReads, to match production; its in-repo fixture shape is kept (it is a valid spec page), and the served shape is pinned in a new file.",
    "Commit trailers use objectui AGENTS' model-free pair (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named trailer. The PR body ends with the session-URL footer the agent definition prescribes, not the harness's 'Generated with' form.",
    "A3 was measured with a scratch probe test inside the worktree (the only place vitest resolves the console project), never committed and deleted; its copy is in the session scratchpad. The worktree is removed: node_modules first, then git worktree remove, exit 0."
    ],
    "files_changed": [
    ".changeset/12045-approval-decision-panel.md",
    ".changeset/12045-request-record-surfaces.md",
    "apps/console/src/AppContent.tsx",
    "apps/console/src/tests/AppContent.approvalRecordReads.test.tsx",
    "apps/console/src/services/approvalRequestsDataSource.ts",
    "apps/console/src/services/approvalRequestsDataSource.decisionPage.test.tsx",
    "apps/console/src/services/approvalRequestsDataSource.recordReads.test.ts",
    "apps/console/src/services/approvalRequestsDataSource.servedPage.test.tsx",
    "apps/console/src/services/approvalRequestsDataSource.timeline.test.ts"
    ],
    "line_budget": "not applicable (no skills/** surface)"
    }


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment 1: domain:ui seat 1 · session_01JZnppgHRp4sREcouhkQYDB · 2026-10-15T05:32Z. It amends the claim 6184502298 with one rider on PR #12432, authorised by the seat in review. The branch, the assignee and Clause-②: no stay.

    The rider: one append-only line on .changeset/12032-approval-requests-data-source.md, so the first 18.x release notes stay true.

    Not part of this rider:


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12045,
    "status": "done",
    "round": "fix round 1 (claim amendment 6185211283, rider on PR #12432)",
    "branch": "claude/issue-12045-page-mount",
    "pr": "#12432",
    "session": "session_01JZnppgHRp4sREcouhkQYDB",
    "premise_still_valid": true,
    "head": "7680faeca0717beda7e368310670ef1edec50fa2 (fast-forward from b688d16, no force)",
    "summary": "Appended one paragraph to .changeset/12032-approval-requests-data-source.md, after its 'Superseded in part (objectui#12045)' paragraph. It says that once PR #12432 lands, the console declares no record route of its own for sys_approval_request; on every surface that opens a request, the console-wide ApprovalRecordReads layer sends the request's single read to GET /approvals/requests/:id and one request's timeline to GET /approvals/requests/:id/actions; and lists of requests, and every other read, stay on the console's adapter, the data API. The frontmatter and every existing line are byte-identical: cmp over the old file's whole 1870 bytes exit 0. No other file.",
    "round_diff_stat": ".changeset/12032-approval-requests-data-source.md | 2 ++ (1 file changed, 2 insertions(+), 0 deletions)",
    "tests": "not applicable (changeset text only; no source or test changed this round)",
    "gates": "At 7680fae, each exit code captured to a file before any read: node scripts/check-changeset-presence.mjs exit 0 ('7 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/12045-request-record-surfaces.md'); node scripts/check-changeset-no-major.mjs exit 0; node scripts/check-changeset-overwrite.mjs exit 0, report-only, naming both modified files (M .changeset/12032-approval-requests-data-source.md, M .changeset/12045-approval-decision-panel.md); pnpm check:changeset-claims exit 0 ('No pending changeset names a file this change touches.'); pnpm check:control-bytes exit 0 ('check-control-bytes: OK'). CI on the new head: not read at report time (not awaited, per contract).",
    "mcp_calls": "0",
    "api_writes": "1 relay stroke plus git push: this os-dev-report comment through scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos//issues/12045/comments). Reads: one REST GET of comment 6185211283. No PR body edit, no label write.",
    "open_questions": [],
    "out_of_scope_findings": [],
    "deviations": [
    "The worktree was recreated from the local branch ref after confirming it equals origin/claude/issue-12045-page-mount (both b688d16). pnpm install --frozen-lockfile ran there so the gate scripts could run. The worktree is removed at the end: node_modules first, then git worktree remove."
    ],
    "files_changed": [
    ".changeset/12032-approval-requests-data-source.md"
    ],
    "line_budget": "not applicable (no skills/** surface)"
    }


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    待裁:审批收件箱点开一条请求时,打开服务端页面还是保留现有侧栏 · domain:ui seat 1 · session_01JZnppgHRp4sREcouhkQYDB · 2026-10-15T05:34Z

    ⛔ 本评论是决策分析,不是认领变更。PR #12432(Refs #12045,B1 挂载步骤中不涉争议的部分)落地后,本卡由 pm:dispatched 转 needs-user-decision;在此之前分析先落卡,维护者可直接回字母。

    一句话问题: 审批人从收件箱点开一条审批请求,今天看到的是手写侧栏,上面有十几项用户报过、已修好的信息面(待谁审批及其职位、金额摘要、原始数据权限闸、已删除记录的墓碑、多步进度条……)。父单 B1 的范围写着「把服务端页面挂到今天侧栏的位置」,但服务端页面目前画不出这些信息面,直接替换会让这些修复全部倒退。

    背景与实测(dev 报告 6185129577 与 PR #12432 正文)

    • 服务端页面 sys_approval_request_detail 在真实 RecordDetailView 上能完整画出:决策面板(计票、批准、驳回、回复)、去掉 record_id 的高亮条、Details 与 Timeline 两个标签页。PR fix(console): every surface reads an approval request through the approvals routes; the served page's timeline counts and labels (objectui#12045, B1 page-mount step, part) #12432 已让所有打开请求的入口(记录路由、ObjectView 抽屉/分屏、两个嵌入记录视图)统一走审批路由读数。
    • 缺口表(侧栏有、页面没有): 待审批人芯片(职位标签与会签分组,objectui#12110 / characterizationPins);声明金额与载荷摘要(声明标签、字段面、隐藏字段裁剪,objectui#11677 / hiddenFieldTrim);流程发起来源;studio.access 才可见的原始数据面板(objectui#5553);目标记录链接的保留/撤回、墓碑与不可解析态(objectui#11878 等);竖向多步进度条。
    • 另:页面的 Details 标签页在 objectui 里是空白(spec 声明省略 sections 时退回 highlightFields,渲染器未兑现),已立 objectui#12433。

    Governing text

    • 裁决 6079807016(乙):「the page is metadata in plugin-approvals … objectui registers the renderers and mounts RecordDetailView」;「⛔ Not taken: 甲 (the placement stays in React, the drift this epic cures; a 甲-then-乙 transition would need named external-user evidence …)」。
    • objectui docs/adr/0060-product-neutral-renderer.md:规则 2「A page that cannot be composed is fixed generically … a generic block in objectui … or a contract door in @objectstack/spec」;规则 3「What already exists stays until its page migrates, and may only shrink.」
    • 父单 objectui#2763 B1 范围(卡正文引):「mounted where the inbox's drawer is today」。
    • 协议声明:本问题不改协议;选项 C 会新增若干通用页面块(spec 行),每行另走契约复核。

    前提(带 re-check)

    • 侧栏信息面均有钉子:git ls-tree --name-only origin/main apps/console/src/pages/system/ | grep ApprovalsInboxPage → 12 个测试文件。
    • 服务端页面只有 actions / highlights / tabs 三个槽:读 objectstack packages/plugins/plugin-approvals/src/sys-approval-request.page.ts。

    选项 × 代价 × 工作量

    选项 做什么 客户可感知后果 工作量
    A 侧栏保留 + 一个「整页打开」链接 侧栏不动,加一条通往请求记录页的链接 审批人体验不变,多一个入口;B1 的「挂到侧栏位置」不做 console · 1 PR · S(约半轮;无直接先例)
    B 页面立即替换侧栏 收件箱点开即服务端页面,删侧栏与其钉子 上面缺口表里的修复全部消失(原始数据闸、墓碑、职位标签等) console · 1 PR · M
    C 页面先补齐信息面,再替换侧栏 先修 objectui#12433;再按拉动逐项补通用块(审批人芯片、载荷摘要、步骤条、记录引用三态、按权限显示的原始面板),每项走 spec 行 + plugin-approvals + objectui;齐了再替换 过渡期审批人仍用侧栏,零倒退;终态全部由元数据驱动 spec + services + ui · ≥5 张卡 · L(先例:B1 本链 objectstack#22472 + #22473 + PR #12062 +1,517/−6)

    业务含义直译: A = 新大楼盖好了但先不搬,门口挂个指路牌;B = 直接搬进还没装修完的新楼,很多设施没了;C = 按清单把新楼装修齐再搬,旧楼期间照常用。

    四维分析(业务立场)

    推荐:C,按 ADR-0060 规则 3 分期:侧栏保留不动(不加链接,规则 3 只许缩小),第一步是 objectui#12433(已欠的实现缺口,无论选哪项都要修),其后每个信息面一张卡、按拉动排序,齐了再替换侧栏。
    回退:A:若维护者认为这些通用块当下不值得扩 spec,则 B1 的挂载从 epic 范围撤下,侧栏作为请求的长期工作面,加一个整页链接。
    置信缺口: 每个新块的 spec 形状尚未设计,C 的「≥5 张卡」是按缺口表计数,不是按设计计数;审批人实际最常用侧栏上哪几项,本席没有使用数据。

    自检:只看①选 C;②③④ 是否翻转:否(② 与 ④ 只影响分期顺序与节奏,不改终态)。

    裁后执行

    os-decision-facets


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed (B1 page-mount step, the part): PR #12432 → main bedf41c1c · pm:dispatched → needs-user-decision · domain:ui seat 1 · session_01JZnppgHRp4sREcouhkQYDB · 2026-10-15T06:02Z

    • Merged: through the merge queue as bedf41c1c, 2026-10-15T06:01Z. One parent, 92f979deb, an ancestor of origin/main.
    • Verified by content: all 10 files the PR changes are blob-identical on bedf41c1c to the accepted head 7680faeca0.
    • Mis-close scan: the PR body carries Refs #12045 and no closing keyword. No issue closed with the merge, and this card stays open.
    • What now holds:
      • Every console surface that opens an approval request reads it through the approvals routes, from one layer (ApprovalRecordReads): the record route, ObjectView drawers and split overlays, and the embedded record views of ObjectDataPage and InterfaceListPage.
      • The served sys_approval_request_detail page draws its decision panel, highlights, and Timeline count and labels with no refused read.
      • The changeset prose is true for one release: amendments 6185211283 and 6081923179.
    • The chain: claim 6184502298, amendment 6185211283, reports 6185129577 and 6185237006, ACCEPT 6185257289.

    Release: session_01JZnppgHRp4sREcouhkQYDB (account sunhaolin) releases the claim 6184502298.

    • Reason: the measured part of the page-mount step has landed. The remainder, what the inbox opens a request on, is a decision, not build work.
    • Destination: needs-user-decision in this act, with the assignee cleared. The decision is 6185241431 above: options A, B and C, with the seat recommending C and A as the fallback.
    • Carried with it:
      • objectui#12433 (the served page's empty Details tab), owed whichever letter is ruled;
      • objectui#12077, which stays pm:blocked on this card.
    • When the maintainer rules, the seat that picks this card up executes the ruling's "裁后执行" section.

    Generated by Claude Code

  13. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    needs-user-decision → pm:blocked · the decision moves to its own card · domain:ui seat 1 · session_01JZnppgHRp4sREcouhkQYDB · 2026-10-15T06:18Z

    Blocked-by: #12438


    Generated by Claude Code

  14. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Blocked-by: re-pointed to the closing card objectui#12484 · domain:ui seat 1 · session_01JZnppgHRp4sREcouhkQYDB · 2026-10-15T11:51Z

    Blocked-by: #12484

    Thread-read: 6185616257


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:workflowApprovals and automation — the work that runs without a person driving itdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpm:blockedpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions