…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
Fixes #8114
packages/plugin-detail/README.mdtaught a detail tab whosecontent.typewasactivity-timeline, which nothing registers. A reader copying the example got the registry'sUnknown component typepanel (OBJUI-001) where the timeline should be.Why
content.typeis a registry-judged positionDetailTabs.tsx:11importsSchemaRendererandtoRenderableSchema;:72handstoRenderableSchema(tab.content)to SchemaRenderer'sschemaprop. A tab'scontentis therefore an SDUI node, and itstypeis 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 byscripts/check-doc-component-types.mjs, builds the 649-key universe that gate judges against.record:activitypackages/plugin-detail/src/index.tsx:673activity-timeline(the old value)activity(bare)skipFallback: truehalfrelated-list,detail-section,record:detailszzz-not-a-component⭐ The controls earned their keep. The first run of this probe read
absentfor every key, controls included:deriveRegistryKeys()returns aMapkeyed by component key, and wrapping it in aSetconstructor sets over ENTRIES, so every lookup missed. Without the controls that run would have become a confident, wrong finding thatrecord:activityis 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 };skipFallbackis what keeps the bare name unclaimed, sorecord:activityresolves andactivityresolves to nothing. Thatactivityis also the tab's ownkeyis a coincidence of spelling.items, notdata.RecordActivityRenderertakes its feed from three sources in precedence order:itemson the node, a mounted discussion context, or a self-fetch fromsys_activityscoped offuseRecordContext. 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.datais read on no path.activityDatais retyped from an array of plain records (theRecordgeneric over string and unknown) to the exportedFeedItem.The pin: what is held, and what is not
Held.
check:doc-snippetscompiles every ts/tsx block inpackages/**/README.mdagainst the builtdist/*.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:So the
FeedItemimport 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
typestring in a package README.check:doc-typesis the gate that judgestypeliterals and it deliberately does not walkpackages/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,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
activityis only a tab key, "not a component type".index.tsxregisters 17 keys;activityis among them, at:673, asRecordActivityRendererlabelled "Activity Timeline". The card's claim is false as written, and the missing entry was the answer.chatter:757,discussion:767,path:776,quick_actions:789,history:817,reference_rail:830,alert:841,permission-facet-link:897, andrelated_list:560.record:activityneeded checking for this position, and it passes — but only withitems. 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/mainwas merged in (commit23bdecd79) rather than rebased, per objectui AGENTS.md — no conflict; the two edits sit in different sections. The docs-side repair for the siblingline-chartinstance landed earlier in objectui#7951.Verification, all on final HEAD
e9c4cc833turbo run build(34-package doc-snippet closure)BUILD_EXIT=0, 35/35 taskscheck:doc-snippetsEXIT=0— 672/672 blocks judged, 0 failed; its own controls fired (sentinel TS2305, positive 0, undeclared TS2307)check:doc-typesEXIT=0check:doc-fences,check:doc-examples,check:doc-example-ids,docs:check-linksEXIT=0changeset:check,check:changeset-claims,check:pending-changeset-literalsEXIT=0pnpm --filter @object-ui/plugin-detail lintEXIT=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.mdis in this package'sfiles, 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