Skip to content

fix(app-shell,plugin-detail): one sys_activity row to FeedItem constructor; stop dropping author-extended types on the console record page - #6731

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-5896-feeditem-single-constructor
Aug 29, 2026
Merged

os-sales merged 1 commit into
mainfrom
claude/issue-5896-feeditem-single-constructor

Conversation

@claude

@claude claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #5896

The defect: what the console record page did with an unmapped type

RecordDetailView's sys_activity merge already read the shared type table (objectui#5878) — and then built the FeedItem itself. The whole of the drop was this:

const feedType = ACTIVITY_TYPE_TO_FEED_TYPE[row.type];
if (!feedType) continue;

One continue, two completely different situations:

  • a type the table maps to undefined on purpose — commented / mentioned / login / logout;
  • a type the table has never heard of.

The second is an author-extended value. sys_activity.type is author-extensible under the objectstack#11507 direction-4 ruling (maintainer, 2026-08-24): every column on sys_activity is readonly: true, so objectql's validateRecord never validates a write to it, and ADR-0052 §5b.2 forwards an author's activityMilestones[].type into the column verbatim — that is how completed is produced, and it is a general door.

What the user saw was nothing. An activity that happened, was written, and is queryable simply had no row on the record page. No placeholder, no empty state, no message on any channel — the row was stored, queryable and invisible. That is objectui#5840's failure mode reached by another route, and the block side had already stopped doing it: objectui#5969 (PR #6112) gave activityRowToFeedItem a defined fallback. So the two surfaces disagreed about the same row of the same table — the block rendered it, the console discarded it.

The convention followed, and whose it is

Not a new one — inherited. activityRowToFeedItem renders an unmapped type through UNMAPPED_ACTIVITY_FEED_TYPE ('system') and warns once per distinct value. 'system' is the generic bucket rather than a new kind because FeedItemType is a closed spec enum owned by @objectstack/spec; minting a kind for "we don't know" would be a platform change, not this surface's. The diagnostic reports a missing decision, not lost data: the row is visible, what it lacks is its own icon and colour.

This PR makes the console surface call that constructor rather than paraphrase it, so the behaviour arrives as a consequence of there being one reading — no second decision was taken here.

⛔ The four deliberate exclusions are untouched. commented / mentioned / login / logout still produce no row and no warning. They are decisions (comment content lives in sys_comment with its own reactions and threading; login/logout are account events, not record activity), and a warning about a decision teaches authors to ignore the channel. That distinction is asserted in its own leg.

What changed

  • packages/plugin-detail/src/index.tsx — the barrel exports the whole reading, not just the lookup table: activityRowToFeedItem, UNMAPPED_ACTIVITY_FEED_TYPE, the resetUnknownActivityTypeWarnings test seam and the SysActivityRow type, alongside the existing ACTIVITY_TYPE_TO_FEED_TYPE. No new module edge: these all come from ./renderers/recordActivityFeed, which the barrel already re-exported from and which renderers/record-activity already pulls in eagerly.
  • packages/app-shell/src/views/RecordDetailView.tsx — the inline for loop calls the constructor. null is now the one outcome that drops a row, and it means exactly one thing. The loop's private timestamp fallback and its second detail.systemActor lookup are gone; the label is passed in, so the console page keeps its own localisation while sharing the construction.
  • RecordDetailView.activityUnmappedType-5896.test.tsx — new.
  • RecordDetailView.activityMapIdentity-5878.test.tsx — one leg updated, see below.
  • .changeset/5896-feeditem-single-constructor.md — minor for both packages (objectui's major tracks @objectstack, so a break ships as minor with the break spelled out).

Three divergences closed, all three named on the card: the silent drop; a timestamp fallback that could hand mergeFeedRows an undefined createdAt where the helper yields ''; and two independently authored i18n lookups for one fallback label.

An existing test leg was updated, and why that is the fix and not a workaround

RecordDetailView.activityMapIdentity-5878.test.tsx had a regression control asserting that zzz_not_an_activity_type produced no row. That leg pinned the silent drop — the defect, not the contract — and it is exactly what the PM annotation on this card warned against carrying forward. It now asserts what survives the ruling: a mapped row shows, a deliberate exclusion does not, and no diagnostic fires for a decision. Nothing was skipped, quarantined or deleted; the unmapped case moved to the new file, where it is asserted positively together with its diagnostic. That file's stale "out of scope" docblock now points at this card.

Pin verification (ablation)

Every pin was run against unfixed source. Method: with the fix committed, packages/app-shell/src/views/RecordDetailView.tsx was reverted to the merge-base blob and the new file re-run, under a trap ... EXIT INT TERM restore.

The consumer is the right ablation subject: the barrel change is a pure export-surface addition with no behaviour, and reverting it too would only make the test file fail to import — a module-load error, not a discriminating red. No rebuild stands between the edit and the run: vitest.config.mts:283 aliases @object-ui/plugin-detail to packages/plugin-detail/src, so no dist is in the resolution path.

Mutation confirmed on disk before the run (not from an editor's exit code): activityRowToFeedItem occurrences 3 -> 0, if (!feedType) continue; occurrences 0 -> 1, blob 02c0d4f0 -> 003daffa. Restore confirmed after: blob back to 02c0d4f0, git diff HEAD empty, working tree clean.

Result — 7 failed, 1 passed:

leg against unfixed source
SUBJECT — the row renders instead of vanishing RED
carries the row, not a husk — summary and actor are the row's own RED
says so — the drop is replaced by a rendered row PLUS a diagnostic RED
warns ONCE per distinct type, however many rows carry it RED
gives the row a STRING createdAt when neither timestamp column is usable RED
IDENTITY — the merge builds its items with the exported constructor RED
hands the constructor this surface's own system-actor label RED
the deliberate exclusions — still dropped, still silent GREEN (control, by design)

The exclusions leg is green in both directions on purpose: it is the counter-probe. Without it, "everything renders" could be reached by deleting the drop entirely.

The identity leg is separate from the behaviour legs deliberately. Every behavioural assertion above is also satisfied by inlining the fallback and the warning into this view — i.e. by re-forking the constructor with today's semantics, which is the drift this card closes and which would pass ON that defect a release later. So a delegating spy is installed over the @object-ui/plugin-detail barrel; it records nothing at all if the view builds its own item.

What was run — all at c5dedf240

  • pnpm --filter @object-ui/plugin-detail --filter @object-ui/app-shell run type-check — both Done.
  • pnpm exec vitest run over the 10 affected files (both RecordDetailView activity pins, feedRecordScope, feedLoading, richtextSurfaceParity, defaults-maps-mirror-en-pack, sharedInboxFeed.rowShape, plugin-detail's recordActivityFeed + record-activity, plugin-calendar's propsContract) — 10 files, 143 tests passed.
  • Gate family derived from the changed paths: check:control-bytes ✅, check:i18n-keys ✅, check:i18n-drift ✅, check:i18n-dead-keys ✅, check:vi-mock-specifiers ✅, check:entry-guard ✅, check:self-import ✅, check:phantom-deps ✅, check:changeset-presence ✅ (3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)), check:changeset-no-major ✅.
  • check:readme-exports — not measured, and not by this diff: it needs every package's dist on disk and only this branch's build closure was built, so it reports 69 unjudged self-imports across plugin-gantt / plugin-map / plugin-markdown / plugin-timeline / plugin-ai / cli / app-shell. plugin-detail — the package whose export surface this PR changes — was judged, and is clean. CI builds everything and runs it for real.
  • check:eager-closure — not measured: it reads apps/console/dist/eager-closure.json, written by a console vite build that was not run here, and it says so itself ("a broken gauge, not a passing budget"). Statically it cannot move: the diff introduces zero new module specifiers — app-shell swaps one named import for another on the same specifier, and the barrel re-exports more names from a module it already re-exported from.
  • Lint, narrowed and declared. Targeted rather than repo-wide (eslint . is CI's run). Three pieces of evidence that the narrowing excluded nothing: (1) the population is eslint's own — each changed file was passed to eslint and it resolved all four as in-scope rather than ignored; (2) the count is read from --format json: 4 results, 0 errors (--max-warnings is deliberately unset in lint.yml, so warnings are not the gate, and the pre-existing warning counts on RecordDetailView.tsx are untouched); (3) eslint.config.js configures no project / projectService, so no rule is type-aware and this diff cannot change the verdict on a file it does not touch — the only cross-file rules are import resolution, and an added export cannot invalidate an existing consumer's import.

Scope fence

⛔ #5877 — the FeedItemType kinds with no producer on any objectui surface — reads the same construction site and is deliberately not touched here. No producer census was taken.

One out-of-scope finding turned up and is filed unassigned as #6730: packages/app-shell/src/hooks/sharedUserFeeds.ts's mapActivityRows is a third hand-written reading of sys_activity.type (targeting ActivityItem, a different four-value vocabulary), which buckets every unrecognised type — author-extended values included — as update, and carries its own copy of the "NOW()" timestamp quirk. It drops nothing, so it is a drift risk rather than a live defect, and converging it needs the ActivityItem vs FeedItem question answered first.


Generated by Claude Code

…uctor; stop dropping author-extended types on the console record page

The console record page's `sys_activity` merge read the shared type table
(objectui#5878) and then built the FeedItem itself, ending in
`if (!feedType) continue;`. That collapsed a DELIBERATE exclusion
(`commented` / `mentioned` / `login` / `logout` -> undefined) with a type the
table has never heard of -- an author-extended value under the
objectstack#11507 direction-4 ruling. The second kind was stored, queryable
and INVISIBLE, with nothing logged: objectui#5840's failure mode reached by
another route, and one the block side had already fixed (objectui#5969 /
PR #6112), so the two surfaces disagreed about the same row of the same table.

- `@object-ui/plugin-detail` exports the whole reading -- `activityRowToFeedItem`,
  `UNMAPPED_ACTIVITY_FEED_TYPE`, `resetUnknownActivityTypeWarnings` -- not just
  the lookup table.
- `RecordDetailView` calls that constructor. The inline loop, its timestamp
  fallback and its second system-actor lookup are gone.
- The four exclusions still produce no row and no warning, deliberately.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 49 chunks) 3233.0 KB 3266.6 KB
Main entry chunk (gzip) 157.3 KB 350 KB
Entry file index-teeo9MBv.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) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
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) 5.13KB 2.35KB
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) 511.59KB 116.29KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 239.31KB 60.18KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.89KB 12.91KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.44KB 34.48KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.43KB 62.46KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.01KB 32.23KB
plugin-gantt (index.js) 165.20KB 40.37KB
plugin-grid (index.js) 201.53KB 54.54KB
plugin-kanban (index.js) 53.11KB 14.62KB
plugin-list (index.js) 113.01KB 27.57KB
plugin-map (index.js) 20.17KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 26.44KB 7.59KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.87KB 21.12KB
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) 67.73KB 22.54KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
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) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
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 (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.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
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

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.

[finding] The sys_activity row -> FeedItem construction is still written twice, and the console surface drops unmapped types silently

1 participant