Repository navigation
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
Activity
- addedenhancementNew feature or requestNew feature or requestpm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreeParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreedomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving it
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: 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:epicstays on it). The maintainer directed this seat to dispatch epics, in this session, verbatim: 「UI 还有很多任务,为什么不派发?包括epic 也可以派发」.
File surface (onmain8f8f760f). 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 (besideRecordApprovalsPanel.tsx, or a sharedrecord-preview/folder). - Reads it may reuse, unchanged: the object-definition read and display-field resolution the console already has, and
useRecordReadabilityfor 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.tsxandRecordApprovalsPanel.tsx: read them, do not change them (B2, B3);- the approvals data source (A1) and the progress widget (A3);
packages/components/src/ui/**andpackages/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/objectuiover 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:noneblocking.- 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
- A new module under
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-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
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsACCEPT —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T07:04Z. PR objectui#12031, headb3fe66cb. This is A2 of the epic objectui#2763.- PR shape:
- Draft against
main, based on8f8f760f.git merge-treeagainstmain8f815f4fis clean. - First line
Fixes #12029, the body's only closing keyword.Clause-②: noat line start. - The commit carries only the model-free trailer pair. Assignee
os-support-ai, set with no refusal.
- Draft against
- 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
patchchangeset 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 indist/index.d.tsanddist/index.js. There is no SDUI component type and no new pack key; the card reads the existingapprovalsInbox.recordUnresolvableandcommon.loading. SoClause-②: noholds, 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 existinguseMetadataItemand the record with onefindOne.- It draws with the record page header's own rules:
getRecordDisplayName(the ADR-0079 title),deriveHighlightFields+detectStatusFieldfor which fields, andHeaderHighlightfor how. - Field-level security:
withoutDeniedFields, and an$expandgated 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:
useRecordReadabilitylives inapps/consoleand 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.- "Absent" means the reference is absent (no object or id: an empty value and zero reads). It does not mean "the record is gone":
findOneresolvesnullon 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 readLookupField,RecordDetailView's embedded mode, plugin-dashboard'sRecordDetailDrawerand 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 Analysisis success. Mergeable stateclean.
- 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
findOnereads. The dev recommends that B2 read the target rows once per object (the readability probe's$inshape) 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 declaredhighlightFieldslist uncapped, whilederiveHighlightFieldscaps 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
- PR shape:
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsLanded —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T07:25Z. PR objectui#12031 merged through the queue as049012bat 2026-10-09T07:23Z.- The merge:
- One parent,
8f815f4f, an ancestor oforigin/main. - The landed tree equals
git merge-treeof that parent and the accepted headb3fe66cb(tree987c9cc1on both). - 3 new files, +615/−0, all three blobs equal to the accepted head's.
- One parent,
- Content check,
8f815f4f→049012b:export function RecordPreviewCardunderpackages/app-shell/srcwent from 0 files to 1. Nothing outsideviews/record-preview/imports it. - Closures: the PR's only closing keyword was
Fixes #12029, and this merge closed objectui#12029 ascompletedat 2026-10-09T07:23Z.pm:dispatchedis removed in this pass.pm:epicstays 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
- The merge:
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 noBlocked-by:.Who acts on it: the objectui
domain:uiexecution seat 3 (seat post objectui#9800, sessionsession_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)
Today the approvals inbox (
apps/console/src/pages/system/ApprovalsInboxPage.tsx, 2,781 lines onmain8f8f760f) 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-shellthat 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:useRecordReadability);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 ownRestart-when:(objectui resolves@objectstack/spec^17until 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 currentmain:RecordApprovalsPanelalready show for a target;(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
main8f8f760fforRecordPreview,ReferencePreview,TargetRecordCardandpolymorphicinpackages/**: no record-preview component. Thepolymorphichits 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