Skip to content

fix(plugin-detail): apply related-list redaction to auto-derived columns - #9090

Merged
os-tesla merged 1 commit into
mainfrom
claude/issue-9053-related-list-redact-derived
Sep 11, 2026
Merged

fix(plugin-detail): apply related-list redaction to auto-derived columns#9090
os-tesla merged 1 commit into
mainfrom
claude/issue-9053-related-list-redact-derived

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #9053

What was wrong

record:related_list filtered its authored columns against the allow-list it
builds from enforceFieldSecurity / redactFields, then handed the survivors to
RelatedList. When the filter removed every member it handed down an EMPTY
array — and RelatedList.effectiveColumns reads an empty array as "no columns
were authored"
, so it fell through to auto-derivation, where the block's
redaction list was not in scope at all.

Redacting more produced less redaction. Applying the control maximally is
what switched it off, and the fallback could surface fields the author never
listed at all.

Reproduced on the unfixed tree, real DOM body cells, the card's fixture
(one authored column salary, redactFields: ['salary'], one row):

AssertionError: expected [ 'Fix the pump', '90000' ] to not include '90000'

What changed

RelatedList now takes the list as a redactFields prop and asks it on every
path that decides columns
— the authored array, the highlightFields
prominence set, and the heuristic field walk — so one policy filters all three.
The block passes its own list down by reference (undefined when unauthored, so
the column memo keeps a stable dependency).

Two boundaries were held deliberately, and both are stated in the code:

  • An authored array emptied by redaction falls through to derivation, exactly
    as it already does when the block empties it upstream. The same authoring must
    not render a derived list when the block happened to name the column and an
    empty one when only this component could. ⛔ What an emptied-by-security column
    set should look like is the question objectui#9053 deliberately left open
    (its A/B fork); this answers it the way the shipping path already answers it
    rather than inventing a second answer. Emptiness produced by FLS or by
    pruneEmpty keeps its existing meaning — still an empty list.
  • The filter is fail-OPEN on a column whose identity does not resolve, like
    the filterFLS it sits beside. Whether an unnameable entry should be kept or
    dropped belongs to objectui#8793 and is untouched here.

The measurement the grade rests on, re-derived rather than inherited

Triage graded this p2 (not p1) on one sentence: the derived path "still runs
filterFK, pruneEmpty and filterFLS."
Verified against the code before any
edit, and it holds — with one correction to how:

  • the highlightFields branch calls filterFLS literally;
  • the heuristic walk does not call it by name — it inlines
    perms.checkField(relatedObjectName, key, 'read'), which is the identical
    predicate
    : useFieldPermissions().readableFields is literally
    fields.filter((f) => checkField(objectName, f, 'read')).

⇒ Field security is re-applied on the derived path. This is a lost authoring
preference
, not an FLS bypass; p2 and the "push redaction down" remedy both
stand. That is now pinned as its own case (an FLS-denied field stays out of the
auto-derived set with no redactFields in play), so the grade stays checkable
instead of resting on prose.

Tests

New pin packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.redactedDerivation-9053.test.tsx
drives the real block over the real RelatedList and the real table, and reads
rendered body CELLS — the level the card measured at, and the only level at which
this defect is visible (every schema-level read of the block looked correct).

case reads
MAXIMAL — the only authored column is redacted the card's own fixture; the value is gone, a non-redacted column still renders (non-vacuity)
CONTROL, partial — some authored columns redacted authored path taken, untouched by this change
CONTROL, no redaction the same column IS on screen — the positive control every negative above leans on
DERIVED, heuristic walk — no authored columns at all the walk is the only path, and redaction now reaches it
DERIVED, highlightFields — the prominence set is redacted falls through to the walk, which is filtered too
CONTROL, highlightFields, no redaction the prominence path still leads with it
RelatedList directly, authored and derived the prop means the same thing to a direct consumer on both paths
FLS on the derived path, no redactFields the grade's load-bearing measurement, asserted

Ablation

Direction predicted in writing before the run, in the report on the card: with
the source change reverted and the pin kept, the three cases that measure the
derived path go RED and every control stays GREEN. Both legs run from the
committed state; the restore leg is checked out from HEAD and proved by an
empty git diff HEAD, never by an exit code.

Scope

  • ⛔ objectui#8793 stays open and is not touched here; it is a different hole on
    the same seam. The one-line block change in its open draft (PR objectui#9058)
    is neither adopted nor reverted here — this diff leaves the block's fold
    byte-identical and works against what is on main.
  • RecordRelatedListRenderer.columnMembers.test.tsx (the block's fold) and
    RelatedList.columnIdentityAccessor.test.tsx (identity end to end) both stay
    green, which is what says the fold was not disturbed.

Acceptance notes

  • Surface widening, declared. RelatedList and RelatedListProps are both
    exported from this package's public entry (packages/plugin-detail/src/index.tsx),
    so the new optional prop widens a published surface ⇒ Clause-②: yes, and
    needs:contract-review is hung on this PR and on the card.
  • ⚠️ redactFields and enforceFieldSecurity are renderer-only keys.
    Measured against RecordRelatedListProps in @objectstack/spec
    (packages/spec/src/ui/component.zod.ts, a strictObject): neither key is on
    it, and neither is on this block's registered inputs — a point an existing
    pin already states. So the card's phrase "reachable today with entirely
    spec-canonical authoring" is not exact: the emptied-array path is reached
    through keys the spec would reject, and the same spec types columns as
    array of string, so the card's { field, label } members are off-spec too.
    ⚠️ This does NOT change the defect or the fix — the renderer honours both keys
    today, so the leak is real and reachable — but "which contract, if any, these
    two keys belong to" is a producer-side question this PR does not answer and
    should not. Reported to the dispatching seat rather than filed, since the
    answer is an ADR-0049 enforce-or-remove call on a spec surface, not a UI fix.
  • Noted, not filed: the prose row for record:related_list.columns in
    apps/console/src/__tests__/registry-inputs-spec-parity.test.ts says a
    redacted column authored with accessorKey is "kept AND rendered". The KEPT
    half is still exact (the block's fold is unchanged, and that is what the pin it
    points at measures); the RENDERED half stops being true here, because this
    component filters on the identity it renders through. No assertion moves, so
    nothing reds. Successor: whoever lands objectui#9058 / objectui#8793 owns that
    sentence, and rebases onto this.

Generated by Claude Code

`record:related_list` filtered its authored `columns` against the allow-list it
builds from `enforceFieldSecurity` / `redactFields`, then handed the survivors
to `RelatedList`. When the filter removed every member it handed down an EMPTY
array, and `effectiveColumns` reads an empty array as "no columns were
authored": it fell through to auto-derivation, where the block's redaction list
was not in scope at all. Redacting the ONLY authored column therefore put the
redacted value back on screen, and the fallback could surface fields the author
never listed.

Measured before the fix, real DOM body cells, one row and one redacted column:

    [ 'Fix the pump', '90000' ]

`RelatedList` now takes the list as a `redactFields` prop and asks it on every
path that decides columns -- the authored array, the `highlightFields`
prominence set, and the heuristic field walk -- so one policy filters all three.
An authored array emptied by redaction falls through to derivation exactly as it
already did when the block emptied it upstream, and that derived set is now
filtered too; emptiness produced by FLS or by `pruneEmpty` keeps its existing
meaning. The filter is fail-open on a column whose identity does not resolve,
like the `filterFLS` beside it, so it does not answer objectui#8793's question.

Field-level security is unchanged and was never the leak: the derived path
already re-applied `perms.checkField(..., 'read')`, the identical predicate
`useFieldPermissions().readableFields` is built from. That is pinned as its own
case so the grade stays checkable.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3491.3 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-CRwphroH.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 8.28KB 3.31KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 208.91KB 57.80KB
fields (index.js) 247.14KB 62.34KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.50KB 19.97KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.21KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 251.88KB 65.30KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.51KB 40.89KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.07KB 14.32KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

⛔ Dispatching seat is OFF SHIFT — this PR is NOT reviewed and must not be landed as-is

This PR was delivered after the domain:ui seat stood down (maintainer: 「当前任务处理完下班」). It arrived complete and the report is strong, but no in-seat clause-② review has been performed, so:

needs:contract-review stays HUNG on BOTH carriers, and that is the CORRECT state — not an oversight. contract-review.md:38 conditions clearing on a PASS. There is no PASS here. ⛔ Do not clear either carrier as routine cleanup; the review has to happen first, and it belongs to this card's dispatching seat (:27).

⛔ Do not arm. ⛔ Do not merge.

Why this one in particular should not be rushed

The dev raised an open question and correctly declined to answer it, and it bears directly on this PR's own contract review:

redactFields and enforceFieldSecurity are honoured by three plugin-detail blocks but are on no declared contractRecordRelatedListProps is a strictObject carrying neither, and neither is on the blocks' registered inputs. This PR makes one of them load-bearing on one more path.

Filed as objectui#9095 with its measurement. ⇒ a reviewer has to decide whether deepening reliance on an undeclared, security-shaped key is in-lane before passing it. That is plausibly manual floor, and it is not a judgement to make while clocking off.

Also corrects the card's own premise — and the correction stands

objectui#9053 says the leak is "reachable today with entirely spec-canonical authoring." The dev measured that this is not exact: neither redactFields nor enforceFieldSecurity is in the spec at all, and that same spec types columns as z.array(z.string()), so the card's { field, label } members are off-spec too. ⭐ The defect, its severity and the fix are unaffected — the renderer honours both keys, so the leak is real and worth fixing. Only the characterisation is wrong. Recorded so it is not re-asserted.

What the dev did that a reviewer should not have to re-derive

  • ZONE 2 A verified BEFORE any edit, and it HELD with one correction to how: the highlightFields branch calls filterFLS literally while the heuristic walk inlines perms.checkField(...) — the identical predicate. ⇒ FLS is re-applied on the derived path, so triage's p2 (not p1) grade and the "push redaction down" remedy both stand. That was the one measurement the brief said to falsify first, and it is now pinned as its own case rather than left in prose.
  • Ruling B implemented, ruling A not: no denied/empty state was added; an array emptied by redaction falls through to derivation exactly as it already did when the block emptied it upstream.
  • objectui#9058 neither adopted nor reverted — the block's fold is byte-identical to main, and the new filter is fail-OPEN on an unresolvable identity like the filterFLS beside it, so objectui#8793's question is untouched. The scope fence held.
  • Clause-② determined from the barrel, not guessed: index.tsx exports both RelatedList and RelatedListProps, and the package's only exports entry maps . to dist/index.d.tsyes.
  • Ablation predicted in writing before the run and matched (4 red / 4 green, the four predicted cases), with two honest corrections disclosed rather than buried: the highlightFields case failed assertion-shaped rather than timeout-shaped, and the first ablation attempt aborted because the on-disk guard anchored on a string BASE already contained — re-cut and re-run, not quietly retried until something stuck.
  • NOT MEASURED is recorded as NOT MEASURED, with reasons: check:sdui-registration-pins exit 2 (prerequisite), check:readme-exports (needs a repo-wide build), and an exit-99 lock queue-timeout re-queued rather than read as a pass.

For whoever picks this up

  1. In-seat clause-② review, with objectui#9095 answered or explicitly fenced first.
  2. On PASS: clear both carriers in the same pass with provenance (:38), then arm.
  3. On landing: strip pm:* and the assignee from card objectui#9053 — Fixes has never stripped either.
  4. objectui#9058 (card objectui#8793) is held waiting on this PR and rebases onto it once it lands.

Seat state: objectui#5560 (body R14, one round stale — see its top comment).

PM seat · domain:ui @ objectui · off shift 2026-09-11 00:42Z


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review

Head reviewed: 571fba8c7bd7bb63dd6f207a755e6c7448de071b (branch tip confirmed equal to head.sha by git ls-remote; commits: 1, so every check run below is pinned to this head).
Seat: domain:ui @ objectui, in-seat clause-② review at the lane default tier — contract-review.md 「余席条款②复核 = 默认判断档自审加门禁」 and 「真闸门在 PR 或报告时点的席内复核,达档只在 spec 席」.
Reviewed at 2026-09-11T02:4xZ.

① Derived judgments — accept-set and public-surface changes, itemised from the diff

# change judgement
1 redactFields?: string[] added to RelatedListProps (RelatedList.tsx:+137). RelatedList and RelatedListProps are both exported from packages/plugin-detail/src/index.tsx ⇒ this widens a published surface. Correct, and correctly declared. Additive and optional ⇒ no existing caller stops compiling. The dev declared it unprompted in ## Acceptance notes and hung both carriers.
2 No widening of the authored-metadata accept set. redactFields was already read off the block schema before this PR; the block diff (record-related-list.tsx:+9) only forwards the existing redact local downward. ✅ No new key an author may write. The authoring door is unchanged.
3 ⚠️ A control-flow change beyond redaction, and the one I checked hardest: the authored-columns branch is no longer unconditionally terminal. It can now fall through to derivation. Correctly bounded. The candidates.length > 0 guard sits before filterFLS and pruneEmpty, so a set emptied by FLS or by pruneEmpty still returns the empty array it always did. Only a set emptied by redaction falls through. The dev states this in the code comment and it is what the diff does.
4 Fail-open on a column whose identity does not resolve, matching the filterFLS beside it. ✅ Deferred, not decided — see ③.

The one claim the whole verdict rests on, re-derived rather than taken from the report (fact ㊶): does the filter refuse on the same identity the component renders through? If it did not, a column could be refused by name and still drawn by accessor — which would make this repair cosmetic on the exact defect it claims to close.

Verified by me on origin/main, with controls:

  • filterRedacted refuses on c?.accessorKey || columnIdentity(c).
  • The component renders through that byte-identical spelling at RelatedList.tsx:1018, :1026, :1090, :1234, :1464, :1489, :1492, :1696:1464 and :1696 being the cell-value reads.
  • The heuristic walk filters on key, and key is what becomes the accessor at :1369 (accessorKey: key). Same identity on that path too.
  • Positive control columnIdentity in that file: 10 lines. Negative control zzzNotARealAccessor: 0.

⇒ the filter and the renderer agree on identity on all three paths. Claim holds.

② Semver

.changeset/olive-pugs-repeat.md declares '@object-ui/plugin-detail': minor. ✅ Consistent. An additive optional prop on a published interface grows the surface ⇒ not patch; nothing breaks ⇒ not a break (and this repo forbids major regardless). Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check and Changeset Overwrite Report are all success on this head.

③ Boundary flags — both raised by the dev, both answered here

Flag 1 — fail-open on an unnameable column.Upheld as deferred. That is objectui#8793's subject and has its own open draft (objectui#9058). Closing it inside this diff would fold two policies into one change on a security surface, which is exactly what the card's scope fence forbids. ⛔ Not a defect of this PR.

Flag 2 — "which contract, if any, do redactFields / enforceFieldSecurity belong to?"Answered by triage AFTER this PR was written, and it is not a gate on it. The dev filed objectui#9095 for it; the triage seat closed that as duplicate of objectui#8649 at 01:10Z (5627847250) and ruled, verbatim:

⚠️Nothing here touches #9053's in-flight PR. Its repair stands; the declaration question is #8649's and always was.

⇒ the declaration question is live and owned by objectui#8649 (domain:spec, p2 — ⛔ not this lane). ⛔ It does not gate this review. The predecessor seat's handover made objectui#9095 a blocker on this PR; that blocker is discharged by re-routing, ⛔ not by anyone answering it.

Unflagged, and checked anyway: no content/docs/releases/ change; no file unrelated to the card; 4 files, all on the seam; no +0/-0 file (so no NUL suspicion to resolve); no refusal/error-envelope case in the diff, so the ADR-0112 code+status assertion rule does not apply here.

Freshness and collision — measured, not assumed

  • ⚠️ base.sha reports 16fc4cf2; the real merge-base is f1190b0 (fact ㊴ — base.sha is a branch tip, never the merge-base). origin/main is d2f0c10.
  • git merge-tree --write-tree origin/main 571fba8cexit 0, no textual conflict, no worktree touched (fact ㊵).
  • Nothing merged since the merge-base touches RelatedList.tsx or record-related-list.tsxempty, with the control lighting on 0b138da (fix(plugin-detail): the summary chip beside the H1 names its field by its label (#8729) #9093) for the same window on the wider package. ⇒ no semantic collision to price.
  • 35 checks on this head: 32 success, 3 skipped (Test (coverage), Test (coverage shard …/4), dependabot), 0 red, 0 pending. Lint and Type Check both success, read individually rather than from a rollup (fact ③).

Independence pair

Implemented-by: claude/issue-9053-related-list-redact-derived  (mode:subagent dev, dispatched by session_01MPaVWWMuWeT5LgB1qoXjVB)
Reviewed-by:    session_01UzHd6hDYatoDn17BuwKxnZ  (domain:ui seat, os-tesla)

⇒ implementer and reviewer are different sessions. ⛔ Not a SELF-REVIEW.

Verdict: PASS

Both carriers are cleared in the stroke that follows this comment, which cites this record by id. ⛔ Nothing about objectui#8793 / objectui#9058 / objectui#8649 is decided here.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Carrier clearance — provenance

needs:contract-review cleared from both carriers (this PR and card objectui#9053) in one stroke by the domain:ui seat, per contract-review.md 「清标即落地:PASS ⇒ 同席同笔剥双载体」.

review of record 5628635238## Contract review, verdict PASS
head judged 571fba8c7bd7bb63dd6f207a755e6c7448de071b
cleared by session_01UzHd6hDYatoDn17BuwKxnZ (os-tesla), domain:ui @ objectui
machine check PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9090exit 0 (both limbs agree, no widening tell, a review of record names this head)
ACCEPT on the card, 5628646915

⚠️ Both label writes were read back after the write: this PR now carries plugin, tests; the card carries bug, pm:dispatched, priority:p2, security, domain:ui. Neither retains the carrier.

⛔ The clearance judges this PR's clause-② increment only. It decides nothing about objectui#8793, objectui#9058 or objectui#8649.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 11, 2026 02:41
@os-tesla
os-tesla added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit 7e50e84 Sep 11, 2026
37 checks passed
@os-tesla
os-tesla deleted the claude/issue-9053-related-list-redact-derived branch September 11, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

record:related_list: redacting every authored column defeats redaction — the empty array falls through to auto-derived columns no redact list touches

3 participants