Skip to content

approvals A2 (child of objectui#2763): a reusable record-preview component that renders a compact card for any (object_name, record_id) pair #12029

Description

@objectstack-fleet

Filing gate ④ — a coordination node: the first child of the cross-layer parent objectui#2763. Parent: objectui#2763 (pm:epic). That parent's restart record (6038179466, triage) orders the split A2 → A1 / A3 → B1–B3, each child its own card, ⛔ the parent never dispatched whole. This is A2, the first in that order, so it carries no Blocked-by:.

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

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

A2 · Polymorphic record-preview component — render a compact card for a (object_name, record_id) pair, resolving the target object + its display fields. Reusable well beyond approvals (audit rows, activity, comments all reference polymorphic targets).

Today the approvals inbox (apps/console/src/pages/system/ApprovalsInboxPage.tsx, 2,781 lines on main 8f8f760f) shows the target as a title alone (r.record_title || formatIdentity(r.record_id), about :724), and the parent names the target-record card as the one missing standard mechanism.

Scope of this card

  • In: one component in @object-ui/app-shell that takes (object_name, record_id) and a data source, resolves the target object's definition, and renders its display fields compactly. It covers four states:

    • loading;
    • readable;
    • unreadable: the record cannot be read, and the card says nothing about why (the inbox's existing readability rule, useRecordReadability);
    • absent.

    Pins for each state.

  • Kept module-internal in this card: no new export from the package entry, and no new SDUI component type. Registering it as a component type that page metadata can reference is B1's step, where it is first consumed. If that registration needs a component type declared in @objectstack/spec, B1 says so in its own Restart-when: (objectui resolves @objectstack/spec ^17 until 18.0 GA, decision: open v18 now and ship it in stages — release the last 17.x from main first without waiting for #21908's deny (A), skip the last 17.x (B), or keep #22009's order (C)? objectstack#22050).

  • Out: the approvals list and detail pages (B1, B2), the approvals data source (A1), the progress widget (A3), and deleting the bespoke inbox (B3).

Premise to re-check before building

The parent was written against v16-era main. Re-read on current main:

  • what the inbox and RecordApprovalsPanel already show for a target;
  • whether any existing component already renders a record compactly from (object, id), such as a lookup-field popover, a related-list row or a record chip.

If one exists, A2 generalises it rather than adding a second.

Duplicate check

  • Semantic issue search on objectstack-ai/objectui (open and closed), polymorphic record preview component object_name record_id compact card approvals A2: 3 hits (1 open, 2 closed). The open hit is the parent objectui#2763; neither closed hit is this component.
  • Source search on main 8f8f760f for RecordPreview, ReferencePreview, TargetRecordCard and polymorphic in packages/**: no record-preview component. The polymorphic hits are the flow inspector's reference pickers.

Dedupe words: polymorphic record preview · approvals target record card · object_name record_id compact card


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-12029-record-preview
    Worktree: objectui-issue-12029
    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 8f8f760f). The card's scope: one reusable record-preview component, module-internal to @object-ui/app-shell.

    • A new module under packages/app-shell/src/views/ for the component, at a path the dev names before writing (beside RecordApprovalsPanel.tsx, or a shared record-preview/ folder).
    • Reads it may reuse, unchanged: the object-definition read and display-field resolution the console already has, and useRecordReadability for the unreadable state.
    • New en and zh rows in the app-shell table the component's copy belongs to, only if a sentence needs one.
    • The tests beside it, and .changeset/12029-*.md.

    ⛔ Not on it:

    • any export from @object-ui/app-shell's entry, and any SDUI component type or registry entry (B1's step);
    • apps/console/src/pages/system/ApprovalsInboxPage.tsx and RecordApprovalsPanel.tsx: read them, do not change them (B2, B3);
    • the approvals data source (A1) and the progress widget (A3);
    • packages/components/src/ui/** and packages/i18n/**.

    Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report).
    Container & model: M, mode:subagent (an in-session subagent), model: opus (dispatch-gates --tier --repo objectstack-ai/objectui over these paths: no path-derived mandate; default tier)
    Clause-②: no
    Responsibility: objectui app-shell: approvals (and audit, activity and comments) point at polymorphic targets, but nothing renders an (object, id) pair as a record, so the inbox shows a bare title | the platform path: the target object's own definition and display fields, read through the console's data source | every approver, and later every surface that references a polymorphic target
    Thread-read: none
    Serial constraints cleared: none blocking.

    • Parent objectui#2763: split started in this pass. A2 is first in the order and carries no Blocked-by:.
    • Open objectui PRs, read 2026-10-09T05:39Z: objectui#12026 and objectui#12028 (app-shell metadata-admin and Studio) touch no approvals file and no new module here.
    • In-flight claims, read 2026-10-09T05:39Z: this seat's objectui#6349 batch 4 (core, types) and objectui#11799 (in the merge queue); seat 2's objectui#11794 (StudioDesignSurface.tsx). None names these paths.
    • First-load budget: a module-internal component that nothing eager imports moves no first-load bytes. If the dev wires it into an eager surface, it measures and reports against the margin ruled in 6070754914.

    Why Clause-②: no: no prop, export, type, registry type or published pack key changes. Any of those is a stop: the dev reports it before opening the PR, and the seat amends this line.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12029,
    "status": "done",
    "branch": "claude/issue-12029-record-preview",
    "pr": "#12031",
    "session": "session_01CGZy1BGCjdN5cXqL9cnvB8",
    "premise_still_valid": true,
    "summary": "Added one module-internal component, RecordPreviewCard, at packages/app-shell/src/views/record-preview/RecordPreviewCard.tsx (tests beside it, patch changeset). It takes objectName, recordId and a data source; reads the definition through the existing useMetadataItem hook and the record with one findOne; and draws it with the record page header's own rules: getRecordDisplayName (ADR-0079 title), deriveHighlightFields(def, detectStatusField(def)) for which fields, HeaderHighlight for how, withoutDeniedFields plus a policy-gated $expand for FLS, all inside a read-only InlineEditProvider so a host page's edit session cannot reach it. Four states, each pinned: loading; readable; unreadable (one identical cause-free card, the inbox's existing approvalsInbox.recordUnresolvable sentence, for a null read, a refused read, a failed read, a throwing source and a missing definition); absent (the pair names no record: EmptyValue, zero reads). Not exported from the entry, no component type, no new pack key. Premise check: no existing compact (object, id) rendering exists (LookupField, RecordDetailView embedded, plugin-dashboard RecordDetailDrawer, global-search hits all read and none fits), so the card composes the record header's three pieces. Two corrections to the brief: (1) useRecordReadability lives in apps/console and imports useAdapter from @object-ui/app-shell, so app-shell cannot reuse it as code (cycle); the card keeps its rule instead. (2) Zone 2's 'absent (the record is gone)' is falsified: findOne resolves null on a 404 and the platform answers 404 for out-of-row-set records on purpose, and the objectui#8631 ruling forbids telling deleted from not-visible; so absent means the REFERENCE is absent, and a platform-asserted deletion stays the caller's to render. Cost: N cards of one object share one definition read (pinned against the real MetadataProvider); each card is one record read, so N ids are N reads (B2 should weigh it).",
    "tests": "All on HEAD b3fe66c from the worktree root. (1) pnpm exec vitest run packages/app-shell/src/views/record-preview/ → 'Test Files 1 passed (1)', 'Tests 13 passed (13)'. (2) Declared narrowing of the package suite: the new test file plus the 21 app-shell tests that enumerate files on disk (grep over the package's tests for readdirSync, globSync, readdir(, walk(, listSourceFiles, import.meta.glob, fs.promises, opendir, tinyglobby, fdir) → 'Test Files 22 passed (22)', 'Tests 174 passed (174)', VITEST_EXIT=0. Argument: the diff adds only new files nothing imports, so an existing test reaches them only through a disk scan; blind spot: a scan through a helper none of those words names. (3) pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json) exit 0; tsc -p tsconfig.test.json --listFilesOnly lists both new files. (4) pnpm --filter @object-ui/app-shell build exit 0, 'dist completeness: 1 package(s) complete (1040 emitted files verified)'; dist/index.d.ts and dist/index.js have 0 hits for the module (control: RecordDetailView 1 hit in dist/index.d.ts), no other emitted file imports it. Ablations via objectstack scripts/ablation-replace.mjs, wrap mode, anchor x1 → x0 and blob change proven on disk, restore proven by blob == HEAD blob (578a746c806c) and empty git diff HEAD after every leg; source-resolved (the test imports ./RecordPreviewCard by relative path, no build in the path). U drop the unreadable branch (comment line → 'return null; // ablation U'): 'Tests 1 failed | 12 passed' (the unreadable pin). First U attempt was a NO-OP: anchor contained in its replacement, tool exit 1 'anchor count moved 1 → 1', nothing ran; redone with a non-overlapping anchor. T break the title rule (getRecordDisplayName(def, row) → String(row.name ?? '')): 'Tests 2 failed | 11 passed' (title pin, N-cards pin). I drop the read-only InlineEditProvider (two anchors, hold then wrap): 'Tests 1 failed | 12 passed' (host-isolation pin). Expected direction red for all three; observed red.",
    "mcp_calls": "0",
    "api_writes": "3 REST writes, all through the fleet relay as objectstack-fleet[bot]: (1) POST /repos/objectstack-ai/objectstack/dispatches carrying pr_create → POST /repos/objectstack-ai/objectui/pulls (draft, objectui#12031; read-back 11244 bytes sent, 11244 stored, identical); (2) POST /repos/objectstack-ai/objectstack/dispatches via label-write.mjs carrying assign → POST /repos//issues/12031/assignees [os-support-ai] (read-back MATCHES); (3) POST /repos/objectstack-ai/objectstack/dispatches via post-stamped.mjs carrying comment → POST /repos//issues/12029/comments (this report). Plus two git pushes (empty branch probe, then b3fe66c), not REST. No label writes, no ready, no auto-merge, no review request.",
    "open_questions": [
    {
    "question": "For B1/B2: should a platform-asserted deletion (an approval's cancelled + record_deleted) reach the card, or stay rendered by the caller?",
    "options": [
    "A: keep the card cause-free; the caller renders the platform's tombstone instead of the card when it holds that assertion (what the card supports today, no new input)",
    "B: add a caller-asserted tombstone input to the card in B1"
    ],
    "recommendation": "A, because: business need: only the approvals rows carry that assertion today, and the inbox already renders it; long-term: the card stays a pure function of the viewer's own read, so it can never borrow the platform's sentence for a fused 404; AI-mistake-proofing: no input exists through which a caller could pass a failed read off as a deletion; startup focus: no new surface until B1 shows a pull."
    },
    {
    "question": "For B2 (a list column of targets): N cards for N ids cost N findOne reads. Batch them?",
    "options": [
    "A: B2 reads the target rows with one list read per object (the readability probe's $in shape) and hands each card its row",
    "B: keep one findOne per card"
    ],
    "recommendation": "A when B2 lands, decided there: it needs a new card input (a pre-read row), which is B2's surface, not this card's."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed · read-only observation: RecordDetailView's own highlight path passes a declared highlightFields list uncapped while deriveHighlightFields caps it at four, and it also maps { name } descriptors the spec (highlightFields: string[]) does not declare",
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed · unmeasured: the synthesized record page's highlights strip passes no label fallback to HeaderHighlight, so a field with no translation bundle entry may draw an empty chip label; the card passes the definition label"
    ],
    "gates": [
    {
    "command": "pnpm exec vitest run packages/app-shell/src/views/record-preview/",
    "exit": 0,
    "verdict": "Test Files 1 passed (1); Tests 13 passed (13)"
    },
    {
    "command": "pnpm exec vitest run --maxWorkers=2 (new test + 21 disk-scanning app-shell tests)",
    "exit": 0,
    "verdict": "Test Files 22 passed (22); Tests 174 passed (174)"
    },
    {
    "command": "pnpm exec vitest run --maxWorkers=2 packages/app-shell/",
    "exit": 143,
    "verdict": "NOT MEASURED: stopped by me at 23m27s under the lock, two agents queued; no results printed"
    },
    {
    "command": "pnpm --filter @object-ui/app-shell type-check",
    "exit": 0,
    "verdict": "tsc --noEmit && tsc -p tsconfig.test.json, no diagnostics"
    },
    {
    "command": "pnpm --filter @object-ui/app-shell build",
    "exit": 0,
    "verdict": "dist completeness: 1 package(s) complete (1040 emitted files verified)"
    },
    {
    "command": "pnpm exec eslint (2 touched source files)",
    "exit": 0,
    "verdict": "2 files, 0 errors, 0 warnings (json); no type-aware linting in eslint.config.js"
    },
    {
    "command": "pnpm check:control-bytes",
    "exit": 0,
    "verdict": "check-control-bytes: OK"
    },
    {
    "command": "pnpm check:test-path-roots",
    "exit": 0,
    "verdict": "check-test-path-roots: OK"
    },
    {
    "command": "pnpm check:changeset-claims",
    "exit": 0,
    "verdict": "No pending changeset names a file this change touches."
    },
    {
    "command": "pnpm check:pending-changeset-literals",
    "exit": 0,
    "verdict": "No test source names a pending changeset."
    },
    {
    "command": "node scripts/check-changeset-presence.mjs",
    "exit": 0,
    "verdict": "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
    },
    {
    "command": "node scripts/check-changeset-no-major.mjs",
    "exit": 0,
    "verdict": "No changeset declares a major bump."
    },
    {
    "command": "node scripts/check-changeset-fixed.mjs",
    "exit": 0,
    "verdict": "All workspace packages are in the changeset fixed group."
    },
    {
    "command": "pnpm check:i18n-keys",
    "exit": 0,
    "verdict": "Every in-scope call-site key resolves against the en pack"
    },
    {
    "command": "pnpm check:i18n-drift",
    "exit": 0,
    "verdict": "No designer-table en value changed in this range."
    },
    {
    "command": "pnpm check:i18n-dead-keys",
    "exit": 0,
    "verdict": "report only"
    },
    {
    "command": "pnpm check:new-line-citations",
    "exit": 0,
    "verdict": "0 new citation(s), enforcement report-only"
    },
    {
    "command": "pnpm check:phantom-deps",
    "exit": 0,
    "verdict": "Every in-scope import is declared by the package that publishes it."
    },
    {
    "command": "pnpm check:self-import",
    "exit": 0,
    "verdict": "No package names itself inside its own src/."
    },
    {
    "command": "pnpm check:published-dist",
    "exit": 0,
    "verdict": "No published package's build output carries tooling material."
    },
    {
    "command": "pnpm check:esm-specifiers",
    "exit": 0,
    "verdict": "no un-ledgered package emits an extensionless relative specifier"
    },
    {
    "command": "pnpm check:unreferenced-sources",
    "exit": 0,
    "verdict": "Every shipped source file in every covered package is reachable (app-shell is not a covered package)"
    },
    {
    "command": "pnpm check:element-data-source-declaration",
    "exit": 0,
    "verdict": "OK"
    },
    {
    "command": "pnpm check:handler-key-reads",
    "exit": 0,
    "verdict": "OK"
    }
    ],
    "deviations": [
    "The dispatch named 'pnpm exec vitest run packages/app-shell/'; the run was stopped (my own PID) after 23m27s holding the shared lock with two agents queued, and replaced by the declared narrowing in 'tests'. The full package run is CI's.",
    "Ablation U's first attempt was a no-op (anchor contained in its replacement; the tool refused, nothing ran); redone and reported.",
    "The claim lists useRecordReadability among reads to reuse unchanged; it cannot be imported from app-shell (it lives in apps/console and imports app-shell), so only its rule is reused. No en/zh rows were needed: the existing approvalsInbox.recordUnresolvable and common.loading keys are read.",
    "A stray 10-byte file /pkgtest.pid (content 'pid=18574') was written at the filesystem root: a backgrounded && chain took my S= assignment with it, so the next line wrote to /pkgtest.pid. Removing it was blocked by the harness safety check; it is left for a person to delete.",
    "The harness attribution reminder asked for a model-named Co-Authored-By trailer; the commit carries objectui AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) instead, per that file's precedence."
    ],
    "files_changed": [
    "packages/app-shell/src/views/record-preview/RecordPreviewCard.tsx",
    "packages/app-shell/src/views/record-preview/RecordPreviewCard.test.tsx",
    ".changeset/12029-record-preview-card.md"
    ],
    "line_budget": "not applicable (no skills/** surface)"
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T07:04Z. PR objectui#12031, head b3fe66cb. This is A2 of the epic objectui#2763.

    • PR shape:
      • Draft against main, based on 8f8f760f. git merge-tree against main 8f815f4f is clean.
      • First line Fixes #12029, the body's only closing keyword. Clause-②: no at line start.
      • The commit carries only the model-free trailer pair. Assignee os-support-ai, set with no refusal.
    • Scope: 3 new files, +615/−0, on the claim (6075037258). No governed path:
      • packages/app-shell/src/views/record-preview/RecordPreviewCard.tsx;
      • its test beside it;
      • a patch changeset on @object-ui/app-shell.
    • Module-internal, checked by the seat: on the head, no file outside views/record-preview/ imports the module. The dev's build shows 0 hits in dist/index.d.ts and dist/index.js. There is no SDUI component type and no new pack key; the card reads the existing approvalsInbox.recordUnresolvable and common.loading. So Clause-②: no holds, and nothing eager imports it, so no first-load bytes move.
    • Diff read (the seat's own):
      • RecordPreviewCard({ objectName, recordId, dataSource }) reads the definition through the existing useMetadataItem and the record with one findOne.
      • It draws with the record page header's own rules: getRecordDisplayName (the ADR-0079 title), deriveHighlightFields + detectStatusField for which fields, and HeaderHighlight for how.
      • Field-level security: withoutDeniedFields, and an $expand gated by the loaded policy.
      • All of it sits inside a read-only InlineEditProvider, so a host page's edit session cannot reach the card.
    • Two corrections to the brief, accepted:
      1. useRecordReadability lives in apps/console and imports @object-ui/app-shell, so app-shell cannot import it without a cycle. The card keeps its rule (one cause-free card for every unreadable cause) rather than its code.
      2. "Absent" means the reference is absent (no object or id: an empty value and zero reads). It does not mean "the record is gone": findOne resolves null on a 404, and the platform answers 404 for a record outside the caller's row set on purpose. The objectui#8631 ruling forbids telling deleted from not-visible. A platform-asserted deletion stays the caller's to render.
    • Premise: no existing compact (object, id) rendering fits. The dev read LookupField, RecordDetailView's embedded mode, plugin-dashboard's RecordDetailDrawer and the global-search hits.
    • Pins and reverse verification (dev report 6075942506):
      • 9 tests over the four states, the title rule, the reference expand, in-place refresh, field-level security on fields and on expand, host isolation, and a shared definition read for N cards of one object (each card is one record read).
      • Three ablations went red as predicted: the unreadable branch, the title rule, and the read-only provider. Every restore was proven (blob equals HEAD).
    • Tests and gates:
      • The new suite passed 13 of 13, and the dev's declared narrowing (the new file plus app-shell's 21 disk-scanning tests) passed 174 of 174.
      • The type-check exits 0, and the build reports dist completeness.
      • The dev stopped the full app-shell run under the shared lock after 23 minutes; CI ran it.
      • CI on b3fe66cb: 43 runs, 40 success, 3 skipped (the expected three), 0 failure. Bundle Analysis is success. Mergeable state clean.
    • Contract review: not required (Clause-②: no).

    Carried to B1 and B2 (the dev's open questions, not this card's):

    • B1: a platform-asserted deletion (an approval's cancelled + record_deleted) stays rendered by the caller. The dev recommends A: no tombstone input on the card. B1 decides it when it registers the card.
    • B2: N cards for N ids cost N findOne reads. The dev recommends that B2 read the target rows once per object (the readability probe's $in shape) and hand each card its row, a new card input that is B2's surface. B2 decides it.

    Findings, noted in the PR's Acceptance notes and not filed (read-only observations, no reach):

    • RecordDetailView's own highlight path passes a declared highlightFields list uncapped, while deriveHighlightFields caps it at four. It also maps { name } descriptors that the spec (highlightFields: string[]) does not declare.
    • The synthesized record page's highlights strip passes no label fallback to HeaderHighlight, so a field with no translation entry may draw an empty chip label. This is unmeasured.

    Landing: ready + auto-merge through the queue, now. Its merge closes objectui#12029. The parent objectui#2763 stays open.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T07:25Z. PR objectui#12031 merged through the queue as 049012b at 2026-10-09T07:23Z.

    • The merge:
      • One parent, 8f815f4f, an ancestor of origin/main.
      • The landed tree equals git merge-tree of that parent and the accepted head b3fe66cb (tree 987c9cc1 on both).
      • 3 new files, +615/−0, all three blobs equal to the accepted head's.
    • Content check, 8f815f4f → 049012b: export function RecordPreviewCard under packages/app-shell/src went from 0 files to 1. Nothing outside views/record-preview/ imports it.
    • Closures: the PR's only closing keyword was Fixes #12029, and this merge closed objectui#12029 as completed at 2026-10-09T07:23Z. pm:dispatched is removed in this pass. pm:epic stays as the subtree's record.
    • The parent: objectui#2763 stays open. A2 is checked on its checklist. A1 (objectui#12032) and A3 (objectui#12033, in flight) follow, then B1–B3, which carry A2's two open questions as recorded in the ACCEPT.

    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

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:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreepriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions