Skip to content

docs(plugin-detail): the Activity tab example names the registered record:activity block - #9827

Merged
os-justin merged 3 commits into
mainfrom
claude/issue-8114-detail-tab-content-type
Sep 18, 2026
Merged

os-justin merged 3 commits into
mainfrom
claude/issue-8114-detail-tab-content-type

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8114

packages/plugin-detail/README.md taught a detail tab whose content.type was activity-timeline, which nothing registers. A reader copying the example got the registry's Unknown component type panel (OBJUI-001) where the timeline should be.

⚠️ Angle-bracket shapes are spelled out in words throughout this body. GitHub deletes tag-shaped and generic-shaped fragments from a stored body, and backticks do not protect them (objectui AGENTS.md, "GitHub rewrites the bytes you write into issue/PR bodies").

Why content.type is a registry-judged position

DetailTabs.tsx:11 imports SchemaRenderer and toRenderableSchema; :72 hands toRenderableSchema(tab.content) to SchemaRenderer's schema prop. A tab's content is therefore an SDUI node, and its type is resolved by the component registry — not a private vocabulary.

Which type replaces it, read off the registry with lit controls

The acceptance fence says to decide by reading the registry, not by guessing. Measured with the repository's own derivation rather than by transcribing the register calls: deriveRegistryKeys(), exported by scripts/check-doc-component-types.mjs, builds the 649-key universe that gate judges against.

key reading
record:activity REGISTERED — packages/plugin-detail/src/index.tsx:673
activity-timeline (the old value) absent
activity (bare) absent — the skipFallback: true half
related-list, detail-section, record:details REGISTERED — lit controls, the probe fires
zzz-not-a-component absent — no false positives

⭐ The controls earned their keep. The first run of this probe read absent for every key, controls included: deriveRegistryKeys() returns a Map keyed by component key, and wrapping it in a Set constructor sets over ENTRIES, so every lookup missed. Without the controls that run would have become a confident, wrong finding that record:activity is unregistered.

Two keys move, because naming the type alone is not enough

  • type: 'record:activity', namespace spelled out. The registration passes the bare name under { namespace: 'record', skipFallback: true }; skipFallback is what keeps the bare name unclaimed, so record:activity resolves and activity resolves to nothing. That activity is also the tab's own key is a coincidence of spelling.
  • items, not data. RecordActivityRenderer takes its feed from three sources in precedence order: items on the node, a mounted discussion context, or a self-fetch from sys_activity scoped off useRecordContext. The last two need a record host, and a bare DetailView element — which is exactly what this example renders — mounts neither. The example's own intent, a caller handing over a feed it already owns, is source one. data is read on no path.
  • activityData is retyped from an array of plain records (the Record generic over string and unknown) to the exported FeedItem.

The pin: what is held, and what is not

Held. check:doc-snippets compiles every ts/tsx block in packages/**/README.md against the built dist/*.d.ts, and it does cover this file. Proven by ablation rather than asserted — mutating the new import to a name the package does not export:

injected FeedItemNotExported: 1     (mutation proven on disk; blob hash changed)
MUTANT_EXIT=1
  [semantic]  packages/plugin-detail/README.md:137:15  TS2305: Module '"@object-ui/types"' has no exported member 'FeedItemNotExported'
  [semantic]  packages/plugin-detail/README.md:139:29  TS2304: Cannot find name 'FeedItem'
restore leg: git diff HEAD empty; blob hash back to e9ce4218 — byte-identical

So the FeedItem import is now genuinely held by a gate.

NOT held, stated plainly rather than left to be mistaken for coverage. No gate in this repository judges the type string in a package README. check:doc-types is the gate that judges type literals and it deliberately does not walk packages/NAME/README.md — that widening is objectui#7896's, and objectui#7896 is blocked by this card. Measured, not assumed: with an unregistered type substituted back into this very block,

type: 'totally-unregistered-block'   (mutation proven on disk)
DOC_SNIPPETS_EXIT=0
DOC_TYPES_EXIT=0

Both gates stay green. ⛔ No gate is added here and no test is invented that would assert a string it also wrote; the corrected type lands as a diff for the next reader, and the loud failure for this whole class arrives with objectui#7896.

Premises falsified

  1. The card's registry census is incomplete — it lists five keys and says activity is only a tab key, "not a component type". index.tsx registers 17 keys; activity is among them, at :673, as RecordActivityRenderer labelled "Activity Timeline". The card's claim is false as written, and the missing entry was the answer.
  2. The dispatch order's census is also incomplete — it lists eight registrations. There are 17: the eight named plus chatter :757, discussion :767, path :776, quick_actions :789, history :817, reference_rail :830, alert :841, permission-facet-link :897, and related_list :560.
  3. record:activity needed checking for this position, and it passes — but only with items. The dispatch was right not to assert it. The renderer's own header states it is drop-anywhere by design; what makes it correct here is that the README mounts no record host, so the host-supplied-feed source is the one that applies.

Serial constraint, honoured rather than hand-ordered

PR objectui#9811 (card objectui#7998) edits this same file and merged at 07:25Z, after this branch was cut. origin/main was merged in (commit 23bdecd79) rather than rebased, per objectui AGENTS.md — no conflict; the two edits sit in different sections. The docs-side repair for the sibling line-chart instance landed earlier in objectui#7951.

Verification, all on final HEAD e9c4cc833

run verdict
turbo run build (34-package doc-snippet closure) BUILD_EXIT=0, 35/35 tasks
check:doc-snippets EXIT=0 — 672/672 blocks judged, 0 failed; its own controls fired (sentinel TS2305, positive 0, undeclared TS2307)
check:doc-types EXIT=0
check:doc-fences, check:doc-examples, check:doc-example-ids, docs:check-links EXIT=0
changeset:check, check:changeset-claims, check:pending-changeset-literals EXIT=0
pnpm --filter @object-ui/plugin-detail lint EXIT=0 (0 errors, 1023 pre-existing warnings)

Heavy runs went through the shared serialisation lock in ../objectstack. Lint used the package's own script, ⛔ never --no-inline-config.

Changeset: patch, following objectui#7989's reasoning for the i18n README — README.md is in this package's files, so the broken example shipped in every tarball and the correction reaches npm readers only through a release.

Session: https://claude.ai/code/session_012EpHzwH4wTy5sd7ibkD2yq

🤖 Generated with Claude Code

https://claude.ai/code/session_012EpHzwH4wTy5sd7ibkD2yq


Generated by Claude Code

…ecord:activity` block

The "With Tabs" example taught a detail tab whose `content.type` was
`activity-timeline`. `DetailTabs` renders a tab's `content` through
`<SchemaRenderer schema={toRenderableSchema(tab.content)} />`, so
`content.type` is an SDUI node position judged by the component
registry — and nothing registers `activity-timeline`. A reader copying
the example got the registry's `Unknown component type` panel
(OBJUI-001) where the timeline should be.

Measured with the repository's own derivation rather than by reading
the register calls: `deriveRegistryKeys()` from
`scripts/check-doc-component-types.mjs` builds the 649-key universe that
gate judges against, and it answers

  record:activity     REGISTERED  <- packages/plugin-detail/src/index.tsx:673
  activity-timeline   absent
  activity            absent          (the `skipFallback: true` half)

against lit controls (`related-list`, `detail-section`, `record:details`
all REGISTERED; a nonsense key absent), so the reading is the gate's,
not a transcription of the source.

Two corrections, because naming the type alone would leave the block
fed by a key it never reads:

- `type: 'record:activity'`, namespace spelled out. The registration
  passes the bare name under `{ namespace: 'record', skipFallback: true }`,
  and `skipFallback` is what keeps the bare name unclaimed — so
  `record:activity` resolves and `activity` resolves to nothing. That
  `activity` is also the tab's own `key` is a coincidence of spelling.
- `items`, not `data`. `RecordActivityRenderer` takes its feed from
  `items` on the node, a mounted discussion context, or a self-fetch
  from `sys_activity` scoped off `useRecordContext`. The last two need a
  record host and a bare `<DetailView>` mounts neither, so the example's
  own intent — a caller handing over a feed it already owns — is source
  one. `data` is read on no path.

`activityData` is retyped from `Record<string, unknown>[]` to the
exported `FeedItem`, which puts the block under `check:doc-snippets`:
that gate compiles every ts/tsx block in `packages/**/README.md` against
the built `dist/*.d.ts`, so the import is now held there.

What is NOT held, stated so it is not mistaken for coverage:
`check:doc-types` is the gate that judges `type` literals, and it
deliberately does not walk `packages/NAME/README.md` — that widening is
objectui#7896's, and it is blocked by this card. The corrected type
therefore lands as a diff for the next reader, not as a gate failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EpHzwH4wTy5sd7ibkD2yq
Picks up objectui#9811 (card objectui#7998), which edits the same file
(packages/plugin-detail/README.md) in a different section.
`README.md` ships in `@object-ui/plugin-detail`'s `files`, so the broken
example went out in every tarball and the correction reaches npm readers
only through a release — the same reasoning objectui#7989 used for the
i18n README's locale count, which is why this is a `patch` rather than
an empty declaration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EpHzwH4wTy5sd7ibkD2yq
@github-actions github-actions Bot added documentation Improvements or additions to documentation plugin labels Sep 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 2 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/7997-detail-view-related-retired.md

  • names packages/plugin-detail/README.md → packages/plugin-detail/README.md — edited by this change

    Documentation. packages/plugin-detail/README.md and content/docs/api/schema-reference.md stop teaching the retired array and gain a migration block each.

.changeset/plugin-detail-8937-parent-scope-residue.md

  • names packages/plugin-detail/README.md → packages/plugin-detail/README.md — edited by this change

    • packages/plugin-detail/README.md (it is in files[], so it ships). It said the node's filter is AND-combined with { [relationshipField]: parentId }, full stop. Since objectui#7299 the parent condition is compiled to match the relationship field's arity, so a multi-valued relationship gets { [relationshipField]: { $contains: parentId } } instead. The paragraph now states both spellings and names the arbiter (@objectstack/spec/data's isMultiValueField). - The claim that the SQL driver decides arity on that same predicate. It does not: driver-sql gates the equality family on its own storage question, which reads multiple as truthy on ANY type. The two rules therefore disagree for a type outside MULTI_CAPABLE_TYPES carrying multiple: true. objectui#9184 moved the arity compiler into @object-ui/core's parent-scope seam and carried the claim with it, so the correction is recorded there — the seam now states the driver's measured rule, records the divergence as a divergence, and points at the upstream card that owns which of the two rules is right (objectstack#17469). RelatedList.tsx's pointer comment and the objectui#7299 test header carried the same sentence and are corrected to match.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with b3ac2d47c (merge-base with origin/main): 1 file(s) changed outside .changeset/, read against 1149 pending declaration(s) that publish a body (1679 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3049.4 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-DS7oxVt3.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) 545.99KB 130.71KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.23KB 62.88KB
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) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
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.83KB 10.95KB
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) 5.52KB 2.10KB
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.92KB 14.22KB
plugin-charts (index.js) 71.61KB 20.05KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.28KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.61KB 57.90KB
plugin-kanban (index.js) 48.10KB 14.94KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.05KB 21.01KB
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) 104.82KB 34.67KB
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) 25.28KB 7.80KB
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) 4.11KB 2.06KB
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.04KB 5.36KB
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

@os-justin
os-justin marked this pull request as ready for review September 18, 2026 11:06
@os-justin
os-justin added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit 592402e Sep 18, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-8114-detail-tab-content-type branch September 18, 2026 11:26
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 plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(plugin-detail): README teaches a detail tab whose content.type is activity-timeline, which nothing registers — a reader gets the OBJUI-001 panel

2 participants