Skip to content

docs(spec): record_related names its row placement inside a parent record - #20948

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20937-record-related-row-words
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20937-record-related-row-words

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20937
Clause-②: no

Pin measurement: the console at this repo's .objectui-sha places no record_related action on related-list rows

Read at objectui db11afd4967cd9d39381c5e21dc2deec9d706204, the pin on origin/main 31c39964fc, with git grep against that commit (never objectui main). git grep -w record_related answers 12 lines in 7 files. The control leg is the same command for list_item in packages/app-shell/src/views/RelatedRecordActionsBridge.tsx: 4 lines, so the read reaches the pinned tree.

  • The related list's default placement never asks for record_related. RelatedRecordActionsBridge.deriveActions (:190, called at :431 and :442) places the child object's list_item actions on each row and its list_toolbar actions in the header. record_related appears nowhere in that file.
  • None of the 12 hits is a related-list placement:
    • Studio designer, 9 lines: i18n labels (4), the inspector's option map (1), the record:quick_actions page-block location option (block-config.ts:536), and ActionPreview.tsx:832-833 (2). The preview draws record_related as a button in a section header, the toolbar reading this card replaces.
    • action-bar.tsx:30 (docblock) and useActionEngine.test.ts (2): the generic location filter. An author who places a record:quick_actions / action:bar naming location: 'record_related' gets that bar where they put it. Nothing places the location by default.
    • ROADMAP.md:1370: prose.
    • The other spelling, record_related_list (containers.tsx:525, RelatedList.tsx:1632 and one test), is a component type name, not this location.

So the new words state the contract only, and they do not say the pinned console does it. The two docs rows describe where a button renders, so each says the console does not place it on those rows yet. They follow the docs corpus's own wording for this state ("declared, not yet applied" in content/docs/ui/translations.mdx) and carry no tracker number.

What changed

  • packages/spec/src/ui/action.zod.ts: the record_related line of the ACTION_LOCATIONS docblock, in triage's words. It is now a per-row action on each row of a related list shown inside a parent record, in that parent's context only. Unlike list_item (every row wherever the object is listed), it never surfaces on the object's own list views. JSDoc only: the enum, its accept set and the global_nav refusal are byte-identical.
  • content/docs/ui/actions.mdx and content/docs/protocol/objectui/actions.mdx: the record_related row of each location table says the same, plus "Declared, not yet placed".
  • docs/qa/platform-checklist/areas/records-forms.json, item records-forms.action-location-matrix: the variant now reads "showcase_log_time on each row of the related-list section inside a record (the Tasks related list on a showcase_project record), and on no row of the showcase_task list view". One addition beyond the claimed line: the same item's revision goes 5 → 6, with a history entry. The checklist README's lifecycle rule for a changed item is "edit the fields, bump revision, append a history entry", because run records pin the revision they ran against. The entry also records that a run against the pinned console scores this variant as a missing placement, and that this is the correct verdict.
  • .changeset/20937-record-related-row-placement.md: @objectstack/spec patch, because src/**/*.zod.ts ships in the package's files[].

No generated file moves. ActionLocationSchema has no .describe(), and the docblock is not projected. check:generated reports "All 15 generated artifacts are up to date", including check:docs ("227 generated files in sync"). content/docs/references/ui/action.mdx lists only the allowed values for ActionLocation.

Verification, at 5e744bd8bf

  • pnpm --filter @objectstack/spec build, then check:generated: all 15 up to date.
  • pnpm --filter @objectstack/spec run typecheck: exit 0, including check:scripts-typecheck and check:test-typecheck.
  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 584 test files passed, 17190 tests passed, 1 todo.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 100 commands. Each exit code was captured to a file before any pipe. --ran reconciled: "100 derived famil(ies) accounted for — 99 run, 1 NOT-MEASURED".
    • 98 exit 0. Five of those first refused with PREREQUISITE NOT MET (lint, formula, client-react and objectql not built). They exited 0 after turbo run build of those four packages.
    • pnpm check:platform-checklist exits 1 with 2 problems: coverage.json · picklist: UNCLASSIFIED and an ABSENT SYMBOL in areas/identity-auth.json. Both are present at the base 31c39964fc: the validator run in a detached worktree at that commit printed the same two problems. The edited item is not among them.
    • NOT MEASURED: pnpm check:dual-build-cjs-loads. It needs every workspace package built (86 dist/ directories absent), and this diff changes one JSDoc comment. CI's Lint & Repo Gates runs it.
  • The six roster families the derivation marks under these paths also exit 0: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:meta-url-spelling and check:spec-changes.
  • Lint, a declared narrowing. ① Population, read from eslint's own config (isPathIgnored / calculateConfigForFile): of the 5 touched files, only action.zod.ts is linted, and the .mdx, .json and .md files are outside it. ② --format json over all 5 lists 1 file: 0 errors, 0 warnings. ③ eslint.config.mjs enables no type-aware linting (no parserOptions.project or projectService), so this diff cannot move any untouched file's verdict.

The two nearby texts the dispatch asked to have measured (not changed here)

  • (a) The GLOBAL_NAV_RETIRED refusal (action.zod.ts:609-622) says "Place the action on a location a renderer serves (… record_related …)". At the pin this is true only in the weak sense. The generic action:bar / record:quick_actions filter renders a record_related action if an author places such a bar with that location. No surface places it by default. It becomes plainly true when the pin carries the renderer half. Changing it now would reword shipped refusal text twice, so I recommend leaving it.
  • (b) The action.locations row in packages/spec/liveness/action.json says the "VALUE SET was audited per-member", which implies every remaining member is served. For record_related at the pin that overstates: no default placement exists. It would take a note-only edit of that row: no status or count change, but a patch changeset, because liveness ships. Or leave it until the renderer half is at the pin.

Acceptance notes

  • skills/objectstack-ui/rules/actions.md:11 (the published skill) carries the same old wording: "Related-list section inside a record". skills/** is a Tier H surface and outside this card's surface. Carrier: none; it needs its own docs PR.
  • packages/lint/src/validate-action-locations.ts:157: the action-no-placement hint lists record_related among the surfaces to add, which is the same weak-sense claim as (a).
  • objectui's Studio ActionPreview.tsx:832-833 (at the pin) draws record_related as a section-header button, the toolbar reading. It is a candidate for the renderer half's scope. That card's scope lists the host bridge, the authored channel, visibility and pins, not the Studio preview.
  • When the objectui pin moves past the renderer half's landing, the "Declared, not yet placed" clause comes out of both docs rows. Carrier: that pin bump.
  • origin/main advanced to f80e2a6dad after the branch point. It touches none of these five paths or packages/spec, so it was not merged in.

Generated by Claude Code

…cord

The ACTION_LOCATIONS docblock line, the two docs location tables and the
platform-checklist variant now state the location's contract: a per-row
action on each row of a related list shown inside a parent record, in that
parent's context only, never on the object's own list views (unlike
list_item). The docs rows state that the console does not place it on those
rows yet. The checklist item bumps to revision 6 with a history entry.

Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:ui tooling labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/ui/action.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/ui/action.zod.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f80e2a6dad85950d9ebde910f5123eae49c2781f → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5e744bd8bfbc45ef8b8cea1a47463b40140ff478
Local-runs: none

Read-only inputs: card #20937 (body, 5919625056, 5919917538, 5920408438, 5920445647), #20936 (5919637359, 5919777544, 5919881057, 5920407001), objectui#11270 (5919760660, 5920436535), PR #20948 body and file list, git diff origin/main...refs/review/pr20948 (merge-base 31c39964fc; the three later main commits touch none of the five paths), objectui at the pin db11afd4967c via git show/git grep, and the head's check-runs. This record judges the diff and the card, not the dispatch or the seat's answers.

① Derived judgments

  • Accept set: unchanged — right. At the head ACTION_LOCATIONS is the same six values, ActionLocationSchema keeps the same error map (only global_nav gets GLOBAL_NAV_RETIRED), ActionLocation is the same type. No .describe() moves. The only change in packages/spec is the four-line record_related entry of the ACTION_LOCATIONS docblock.
  • No generated artifact moves — right. A git grep of origin/main for the old phrase finds six sites, all hand-written: the docblock, the two docs rows, two checklist lines and skills/objectstack-ui/rules/actions.md:11. content/docs/references/ui/action.mdx lists the bare enum values; api-surface, json-schema and spec-changes.json carry no docblock text. The Flag docs affected by code changes, Build Docs, Check Documentation Links and Spec property liveness runs are green.
  • The contract words match triage's ruling (5919625056) — right. Docblock: "per-row action on each row of a related list shown inside a parent record, in that parent's context only", "never surfaces on the object's own list views", contrasted with list_item (every row wherever the object is listed). Both docs rows say the same three things (row placement, parent-record scope, never on the object's own list views) and agree with each other and the docblock. Nothing says toolbar or section header.
  • "Declared, not yet placed" is TRUE at the pin — right. objectui db11afd4967c, packages/app-shell/src/views/RelatedRecordActionsBridge.tsx: deriveActions (:187) takes location: 'list_item' | 'list_toolbar' and is called at :431 for rows and :442 for the header. git grep -w record_related at that commit: 12 lines in 7 files, none a related-list placement (Studio i18n labels x4, ActionDefaultInspector.tsx:206, block-config.ts:536, ActionPreview.tsx:832-833 which draws a section-header frame, the generic action-bar.tsx:30 docblock, useActionEngine.test.ts x2, ROADMAP.md:1370). The dev's count and the control leg reproduce. The wording follows the corpus's own precedent for this state (content/docs/ui/translations.mdx:382, plugins/development.mdx:268, permissions/sharing-rules.mdx:140).
  • Docs-authoring rules — met. check-doc-authoring Rule 1 is about ts fences (none touched); Rule 2 (bare tracker ids) is scoped to skills/** by its own header and the rows carry no id anyway; both pages are hand-written trees per AGENTS.md's docs table, no meta.json change is due, and no gate mirrors the two location tables (check-corpus-claim-drift is the $exists lexical ratchet; handwritten-docs.json only lists the pages).
  • The checklist variant's new expectation is grounded on main — right. examples/app-showcase/src/ui/actions/index.ts:204-217: showcase_log_time, objectName: task, type: 'form', target: 'showcase_task.edit', locations: ['record_header', 'record_related', 'record_section']. task.object.ts:38-42: project: Field.masterDetail('showcase_project', { relatedListTitle: 'Tasks', relatedListColumns: [...] }); relatedList absent means shown (field.zod.ts:1525), so the Tasks related list on a showcase_project record exists. The action declares no list_item, so "on no row of the showcase_task list view" holds under both the contract and the pin's bridge.
  • revision 5 to 6 plus the history entry — right, and required. README "Lifecycle: Change — edit the fields, bump revision, append a history entry"; the validator (check-platform-checklist.mjs:2741-2751) requires revision to equal the last entry's and each entry to carry {revision, date, change}. The entry has all three plus ref: "#20937" (the README's shape comment says a PR, the validator does not check it, and the item's own earlier entries use branch names and #10236; not a defect). enumSource.expect stays 6, matching the enum.
  • History text — accurate. The old line is quoted correctly; the bridge behaviour at the pin is as stated; the renderer half is objectui#11270; "a run against that pin records this variant as a missing placement" is the item's first acceptance clause ("zero missing placements") taking RUNNER.md's fail verdict with objectui#11270 as the filed issue. That clause was already failing at the pin before this diff (the action declares no list_toolbar, so it never rendered in the section header either); the diff makes the expectation precise, it does not create the failure. Keeping the item active is right: planned is item-level for "nothing to drive", and six other variants run.

② Semver level

  • What the diff publishes: one JSDoc block in packages/spec/src/ui/action.zod.ts, shipped because src/**/*.zod.ts is in @objectstack/spec's files[]. content/docs/**, docs/qa/** and .changeset/ publish nothing.
  • @objectstack/spec patch — right. A published file changes with no accept-set, export or behaviour change; skip-changeset would be wrong (AGENTS.md Post-Task step 3: that label is for a diff publishing nothing). Precedent: .changeset/19518-picklist-wording.md (docs(spec), patch, Clause-②: no).
  • Clause-②: no — right. No key, no enum member, no accept-set change; the PR body and the changeset carry the same line.
  • Changeset prose — no false claim. "Only the record_related line of the ACTION_LOCATIONS docblock in src/ui/action.zod.ts changes" is true within the package; "the same six values parse, and no .describe() string, export or runtime behaviour moves" is true at the head; "the console's placement ... ships separately" is true (objectui#11270, then a pin bump).
  • The summary's (#20937) — allowed. check-doc-authoring's ROOTS are .claude, docs, skills, content; its Rule 2 header scopes the tracker-id ban to skills/** alone; AGENTS.md's tracker-number rule covers runtime strings and its own rules. The published packages/spec/CHANGELOG.md on main carries 2034 (#NNNN) summaries, docs(spec): ... (#17670) among them. Check Changeset is green.

③ Boundary flags

  • (3a) GLOBAL_NAV_RETIRED lists record_related among served locations; seat answered A (leave it) — judged right. The list is the contract's location set, equal to ACTION_LOCATIONS. At the pin the generic action:bar / record:quick_actions location filter (action-bar.tsx:30, useActionEngine.test.ts:111) renders an authored bar at that location, so "a renderer serves" holds in that sense today. The ruling is enforce, so the text becomes plainly true at the pin move; dropping and restoring a shipped refusal string would cost two patch changesets for an interim state. No escalation.
  • (3b) liveness action.locations note "VALUE SET was audited per-member"; seat answered A (leave it) — judged right for this diff, with a carrier. The row's status: live is the KEY's, evidenced by objectui's actionRendersAt, and the note records the global_nav action location renders nowhere in the running app — the ⌘K palette never reads action metadata, but Studio previews a command-palette frame for it #6888 audit as history; it asserts no default placement per value. But a ledger reader takes "audited per-member" as every member served, and spec(ui): the record_related action location ("actions on a related list section inside a record") has no read site in objectui, while the showcase and a platform-checklist item rely on it (ADR-0049 enforce-or-remove) #20937 measured one member as declared-not-placed. Escalated: a note-only patch on that row belongs to the pin bump that carries objectui#11270 (Bump .objectui-sha past objectui f4ed2387e9 (objectui#11163, related-list actions), which also carries objectui#11199: unblocks #20936 and #20299 #20949 names it as a rider), not to this words-only claim.
  • Dev flag: skills/objectstack-ui/rules/actions.md:11 keeps "Related-list section inside a record"; carrier none. Stale but not false about the scope; it is the corpus AI authors from and now coarser than the spec's own words. Escalated to the seat: it needs its own Tier H docs PR or a rider on the pin bump; it does not block a packages/spec words PR.
  • Dev flag: validate-action-locations.ts:157 hint. Same weak-sense list as (3a); same disposition.
  • Dev flag: Studio ActionPreview.tsx:832-833 draws record_related as a section-header button. Confirmed at the pin. Relayed to objectui#11270 as a scope note (5920436535); that is the right carrier, since the ruling replaced the header reading it previews.
  • Dev flag: the "Declared, not yet placed" clauses come out at the pin move. Recorded in 5920436535; the carrier is the .objectui-sha bump that carries objectui#11270.
  • Dev flag: check:platform-checklist red at base (2 problems, neither this item). The gate is kept out of per-PR CI by maintainer decision (lint.yml:3061-3067) and platform-checklist-watchdog.yml runs it on main; not this PR's.
  • Deviations reported: the revision bump (required by the README; the seat amended the claim in 5920445647); the premise correction (12 hits at the pin, not zero; none a placement, so the premise holds, and this review reproduced the count); no main merge (none of the three later commits touch the five paths); the model-free commit trailer (AGENTS.md's rule); lint narrowed to the touched files and check:dual-build-cjs-loads NOT MEASURED locally. Build Core, which hosts check:dual-build-cjs-loads (ci.yml:2123), is green on the head; Lint & Repo Gates answers the lint narrowing and was in progress at this read.
  • Check-runs on the head at this read: 34 runs, 0 failed; 27 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in)), 5 in progress (Lint & Repo Gates, Test Core 1, 3, 4 and 5 of 6). Green: Build Core, Build Docs, Check Changeset, Check PR Size, Spec property liveness, TypeScript Type Check and all four Type Check · jobs (source gates, consumer gates, debt ledger, workspace), Test Core 2 and 6 of 6, Dogfood Verify CLI, Dogfood Regression Gate and its 3 shards, Temporal Conformance (live PG + MySQL), Flag docs affected by code changes, Check Documentation Links, Governed Surface Queue Guard and the claim guards. The Lint & Repo Gates run, which hosts check:doc-authoring, check:generated and the eslint verdict, was still in progress; its conclusion on this head governs those families.

Implemented-by: claude/issue-20937-record-related-row-words
Reviewed-by: session_018fxqvRJW12TaHC7DUQ89Y6

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 22:14
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 013f97d Sep 30, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20937-record-related-row-words branch September 30, 2026 22:36
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…its child's `record_related` actions on each row (objectui#11270) (objectstack-ai#11289)

Fixes objectstack-ai#11270
Clause-②: yes

A related list inside a record now places its child object's
`record_related` actions on each row. The authored `actions` channel on
`record:related_list` accepts them too, and the Studio action designer
previews that same row placement instead of a section-header button.
This is the renderer half of the enforce answer on
objectstack-ai/objectstack#20937 (triage `5919625056`): row placement,
only inside a parent record.

Implemented by the `domain:ui` seat 1 dispatch, session
`https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ`, on claim
`5920737104`.

## What changed

- **Host bridge**
(`packages/app-shell/src/views/RelatedRecordActionsBridge.tsx`,
`deriveActions`). The row menu now reads `list_item`, plus
`record_related` when a parent record is in scope (`parentRecordId`
set). The check goes through `actionRendersAt`, the platform's one
placement rule. An action that declares both locations renders once, in
the child's declared order. The toolbar still reads `list_toolbar`
alone.
- **Authored channel**
(`packages/plugin-detail/src/renderers/relatedListActions.ts`). An id
whose action declares `record_related` is placed in each row's menu, in
authored order. The refusal notice for an id that cannot be placed now
names the three related-list locations: `list_toolbar`, `list_item` and
`record_related`.
- **Designer preview**
(`packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx`,
`PlacementPreview`). `record_related` no longer shares the
`record_section` frame and its section-header button. It draws the row
frame `list_item` already had: the same element, reused, under its own
caption. No new frame family was added. `record_section` keeps its
header frame. This takes in the cross-lane scope note `5920436535`.
- Doc comments that described row actions as `list_item` only now name
both row locations: `RelatedList.tsx`, `record-related-list.tsx`, and
`RelatedRecordHandlers.rowActions` in `@object-ui/react`, which is a
comment-only change. The guide `content/docs/guide/slotted-pages.md`
explains how the two locations differ in scope.
- Changeset `.changeset/11270-record-related-row-placement.md`:
`@object-ui/app-shell` minor, `@object-ui/plugin-detail` minor,
`@object-ui/react` patch (comment only).

Unchanged: the `list_item` placement, the child object's own list view,
and the row visibility gate (see measurement 3).

## Measurements (dispatch Zone 2)

1. **No surface read `record_related` (confirmed).** The four new pin
files run against `e420df310f`: **11 failed, 6 passed**. The 6 that
passed are the controls and the negative halves. This is commit
`7c50fdac8a`, pushed before the implementation.
2. **How the bridge knows it is inside a record.**
- `RelatedRecordActionsBridge` has one mount site, `RecordDetailView`.
It wraps the record page body there, with `parentRecordId` set from the
route's record id.
- The list page publishes `listRecordActionsValue`, and its `resolve`
answers `NO_RELATED_HANDLERS`. A related list under it therefore gets no
row actions at all.
- The child object's own list view is `ObjectView`. It composes
`rowActionDefs` filtered by `list_item` alone.
- The bridge's props document a mount with no parent ("Omit when there
is no parent context (e.g. a standalone list)"). The rule is therefore
explicit at the bridge rather than left to the mount site:
`record_related` joins the row set only when `parentRecordId` is set.
- The negative is pinned twice. The child's own list view does not carry
a `record_related`-only action, with the `list_item` control carried by
the same render. A bridge with no parent record does not place it
either.
3. **The row gate: it is the second definition. Reported here and not
widened.**
- The related-list row menu path is: `RelatedList`, then the data
table's `rowActionDefs`, then `planDataTableRowMenu` /
`DataTableRowActionItem`. Both of those ask `isCustomRowActionVisible`
in `packages/components/src/renderers/complex/data-table.tsx`.
- That function has its own test (`pred == null` or `pred === ''`). It
is not `hasDeclaredVisibilityGate`, which is core's
`hasDeclaredPredicate`; that one also reads whitespace-only text and an
empty envelope as undeclared.
- `packages/plugin-grid/src/components/RowActionMenu.tsx` holds a second
function under the same name.
- `record_related` rows go through the same path as `list_item` rows, so
this PR adds no gate. See Acceptance notes.
4. **The designer preview.** The `record_related` frame was the
section-header frame shared with `record_section`. It now draws the
`list_item` row frame, so no new frame family was needed.
5. **The refusal notice** names `list_toolbar`, `list_item` and
`record_related`. It is pinned on the pure function and through the real
renderer.

## Pins

-
`packages/app-shell/src/views/__tests__/RelatedRecordActionsBridge.recordRelated-11270.test.tsx`:
the real bridge around the `{ type, properties }` node, rendered through
the real `SchemaRenderer`, `@object-ui/plugin-detail`'s registration,
the real `RelatedList` and the real data table. It covers:
- a `record_related`-only action on the row, beside the `list_item`
control;
  - an action declaring both locations renders once;
  - it runs against the clicked row, retargeted at the child object;
  - a bridge with no parent record places `list_item` only;
- a named `record_related` id in the authored channel is accepted and
nothing is refused;
  - the notice names the three locations.
-
`packages/app-shell/src/views/ObjectView.recordRelatedNotOnOwnList-11270.test.tsx`:
the child's own list view carries `send_reminder` and `archive` (via its
`list_item` half) and not `log_time`.
-
`packages/plugin-detail/src/renderers/__tests__/relatedListActions.recordRelated-11270.test.ts`:
placement and notice on the pure functions.
-
`packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.recordRelatedRow-11270.test.tsx`:
`record_related` draws rows and no section header. `record_section`
keeps its header (the control). Each location draws its own frame.

## Verification (final head `b04aaba6d2`)

- Red first on `e420df310f`: 11 failed / 6 passed. Green on the
implementation: `Test Files 4 passed (4)`, `Tests 17 passed (17)`.
- Ablation, run once with a revert trap, not a permanent test:
- Mutation: `hasParentRecord ? ROW_LOCATIONS_IN_RECORD : ROW_LOCATIONS`
became `ROW_LOCATIONS_IN_RECORD`, through `ablation-replace.mjs`. It
landed: anchor 1 to 0, blob `c694d015fadb` to `727fa08500b1`.
- Predicted and observed: 1 failed / 5 passed, and the failure is the
parent-less case.
  - Restore proven: blob equals HEAD, and `git diff HEAD` is empty.
- The first attempt was refused by the tool before running, because the
replacement text already occurred in the anchor and its count could not
rise. It was redone with a marker.
- `pnpm exec vitest run packages/plugin-detail/`: `Test Files 228 passed
| 1 skipped (229)`, `Tests 2270 passed | 8 skipped (2278)`.
- `pnpm exec vitest run` over
`packages/app-shell/src/views/metadata-admin/previews/`,
`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`, and
every suite `git grep -l` finds for `record_related`, `deriveActions` or
`RelatedRecordActionsBridge`: `Test Files 105 passed (105)`, `Tests 1524
passed (1524)`.
- Build: `turbo run build --filter='@object-ui/app-shell^...'
--concurrency=2`, 28 of 28 tasks successful.
- Type-check exit 0 for `@object-ui/plugin-detail`, `@object-ui/react`
and `@object-ui/app-shell`. Each runs `tsc --noEmit && tsc -p
tsconfig.test.json`, and `--listFilesOnly` confirms that all four new
test files are in the test programs.
- Repo checks, all exit 0: `check:action-forward-parity`,
`check:component-surface-parity` (report-only),
`check:handler-key-reads`, `check:new-line-citations` (`0 new
citation(s)`), `changeset:check`, `check:control-bytes`, `node
scripts/check-changeset-presence.mjs`, `check:test-path-roots`,
`check:vi-mock-specifiers`, `check:vi-mock-inherit`,
`check:vi-mock-override-shape`, `check:changeset-claims`,
`check:pending-changeset-literals`, `check:doc-fences`,
`check:doc-example-ids`, `check:doc-example-readers`,
`check:action-ref-convention`, `check:i18n-dead-keys`,
`check:unreferenced-sources`.
- NOT MEASURED:
- `check:doc-snippets` and `check:doc-examples` stopped with exit 2,
precondition not met: seven packages outside this build's closure are
unbuilt. The guide edit is prose only and changes no fenced block.
- The repo-wide `pnpm lint` is left to CI. A narrowed `eslint --format
json` over the 10 changed `.ts` / `.tsx` files reports 10 files and 0
errors. The warnings are the files' existing `no-explicit-any` /
`react-refresh` / `set-state-in-effect` findings, and one `as any` in
the new ObjectView test's data-source double, the same spelling its
sibling suites use. `eslint.config.js` declares no type-aware parser
project, so this diff cannot move a verdict on an untouched file.
- Governed surface: `node scripts/check-governed-queue-guard.mjs --test`
over the 12 changed paths answers NOT GOVERNED.

## Acceptance notes (observations, not filed)

- **Row-gate second definition (measurement 3).**
- `isCustomRowActionVisible` exists twice, in `data-table.tsx` and in
plugin-grid's `RowActionMenu.tsx`. Each asks "is a gate declared?" with
its own test instead of `hasDeclaredVisibilityGate`.
- It differs in its reading of whitespace-only predicate text and of an
empty envelope.
- This is the same family as objectui#11244 (PR objectui#11275 moved the
related-list toolbar onto the shared helper).
- Not widened here, per the dispatch. Reach through a public door was
not measured.
- **Mobile.** Under the 768 px breakpoint a table-type related list
renders as a card gallery with no row menu. Neither `list_item` nor
`record_related` actions appear there. This predates this PR and is
unchanged by it.
- **`isRecordScopedAction`**
(`packages/core/src/actions/serverActionHandler.ts`) lists `list_item`,
`record_header`, `record_more` and `record_section`, but not
`record_related`, although objectstack's runtime counts `record_related`
as record-requiring. It only decides a toolbar launch with nothing
selected, and a `record_related` action is never placed on the toolbar.
- **Follow-on in objectstack**, not this repository:
- PR objectstack-ai/objectstack#20948's "Declared, not yet placed"
clause in the two docs location tables comes out at the objectstack
`.objectui-sha` move that carries this merge.
- The spec describe and docs wording follow on
objectstack-ai/objectstack#20937.
- **File surface.** The dispatch expected the bridge under
`packages/plugin-detail/src/`. It lives in
`packages/app-shell/src/views/RelatedRecordActionsBridge.tsx`, so
`@object-ui/app-shell` carries both the bridge and the preview changes.

---
_Generated by [Claude
Code](https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/s tooling

Projects

None yet

2 participants