Skip to content

fix(plugin-detail): record:activity renders scheduled activities as event - #5886

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5840-activity-scheduled-kind
Aug 23, 2026
Merged

fix(plugin-detail): record:activity renders scheduled activities as event#5886
os-zhuang merged 1 commit into
mainfrom
claude/issue-5840-activity-scheduled-kind

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#5840

sys_activity rows with type: "scheduled" were discarded before any filter ran, so a scheduled meeting could never appear on any activity timeline whatever the page authored. This maps scheduled to the event feed kind.

Re-measured at live source

The card measured a shipped bundle. The live source is packages/plugin-detail/src/renderers/recordActivityFeed.ts, and it says the same thing — ACTIVITY_TYPE_TO_FEED_TYPE had no scheduled key, and activityRowToFeedItem returns null for anything that resolves falsy, upstream of showCompleted, upstream of types, upstream of everything a page can configure.

Table as it stood, read from source, not from dist:

sys_activity.typefeed kindafter this PR
createdupdateddeletedassignedsharedfield_changeunchanged
completedtaskunchanged
systemsystemunchanged
commentedmentionedloginlogoutundefined (dropped)unchanged — see below
scheduledabsent from the tableevent

The producer, measured

objectstack-ai/hotcrm at 5eee1bd, src/actions/global.actions.ts:

constactivity=awaitctx.api.object('sys_activity').insert({type: EVENT_STATUS==='held' ? 'completed' : 'scheduled',

registered for crm_lead, crm_contact, crm_account, crm_opportunity, crm_case. The held branch reached the timeline; the scheduled branch was written, stored, queryable and invisible.

scheduledevent also makes the declared event feed kind reachable for the first time, which answers the second half of the card's complaint at zero extra cost.

commented / mentioned are NOT mapped — what was searched

The PM's ruling was to map them only on a measured producer. There is none:

  • objectstack's own writer census (packages/plugins/plugin-audit/src/objects/sys-activity-type-vocabulary.test.ts) records both explicitly: commented — "no writer: sys_comment hooks write comment rows, never a sys_activity row"; mentioned — "no writer: the @mention hook notifies; it emits no activity row".
  • grep -rn "'commented'\|'mentioned'" src across hotcrm returns nothing.
  • Independently, mapping them would double-render: the console record page already loads these from sys_comment to get reactions and threading, and its own comment says so.

showCommentInput / enableMentions are props of this component, not producers of those row types. Left unmapped.

The unknown-type default is unchanged, deliberately

A type nothing maps is still dropped, not bucketed into system — rendering an unmeasured kind as something it is not is new wrong data, not recovered data.

What changed is that the drop is no longer silent: one console.warn naming the type, deduped per type (same shape as the evaluator's loud fail-open in core/src/evaluator/fieldRules.ts). Types the table knows and deliberately excludes stay silent, because a warning about a decision teaches authors to ignore the channel.

What the card's premise got wrong, and one thing it did not know

The defect itself reproduces exactly. Two corrections:

  1. "10 of 13 kinds have no producer" counts only the sys_activity map. comment does have a producer — sys_comment rows, via the host discussion context. After this PR the true count is 8: email, call, note, file, record_create, record_delete, approval, sharing. Filed as [finding] 8 of FeedItemType's 13 kinds still have no producer on any objectui surface #5877 per the PM ruling below.
  2. scheduled is not in the platform's declared vocabulary at all. plugin-audit declares eleven options and scheduled is not among them. It lands anyway because every field on sys_activity is readonly: true and objectql's validateRecord skips readonly fields, so an undeclared value is stored silently — measured upstream, in that same census file. The platform also forwards author-declared values into the column (ADR-0052 §5b.2 activityMilestones[].type, applied verbatim by audit-writers.ts), so this is a general door rather than one app misbehaving.

That second point is why the test's key-set pin had to change rather than just gain a row. It asserted set-equality with plugin-audit's declared options; that premise is false, and keeping it would mean dropping stored rows to stay tidy. The pin now states both vocabularies — the declaration, plus values a shipped producer measurably writes, each naming its producer, as literals for the same reason the upstream census uses literals.

Whether the upstream enum should absorb scheduled is a platform ruling, filed as objectstack-ai/objectstack#11424.

PM ruling carried into this PR

From the domain:ui PM seat: take the first direction only. Extend the map so the vocabulary sys_activity actually stores reaches the timeline; do not narrow FeedItemType, since that enum is published by @objectstack/spec in the other repo and narrowing it is a cross-repo spec retirement with ADR-0087 registry obligations. The ten-unreachable-kinds half is recorded separately as #5877 rather than acted on here.

Verification

Every claim below quotes the verdict line the tool printed. Exit codes captured before any pipe.

Tests — path-filtered from the repo root, at final commit 1df1c03d3:

os-verify-lock: VERDICT command-exit 0 · held the lock 10s
Test Files 2 passed (2)
Tests 49 passed (49)

Dependency closure built first: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' buildVERDICT command-exit 0.

Typecheckpnpm --filter @object-ui/plugin-detail type-checkVERDICT command-exit 0, and the run echoed the script, so it was not a zero-match no-op:

> @object-ui/plugin-detail@17.6.0 type-check
> tsc --noEmit && tsc -p tsconfig.test.json

Gates:

gateverdict line
check-changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major✅ No changeset declares a 'major' bump.
check-control-bytes✅ check-control-bytes: OK (scanned 4890 tracked text file(s); skipped 85 binary).
check-doc-linksLinks are valid across 13 scan roots.
check-doc-component-types✅ Every documented component type is registered.
check-i18n-call-site-keysEvery in-scope call-site key resolves against the en pack (2928 keys)
check-i18n-en-driftNo en value changed in this range.
check-package-self-import✅ No package names itself inside its own src/.
check-spec-symbol-derivation✅ spec symbol derivation: 1300 files scanned against 4959 spec export names

check-doc-snippet-typesdid not evaluate and is declared, not claimed: it exits 1 on a prerequisite, not on content — The snippet program was NOT run: the packages it resolves against are not built, naming nine unbuilt packages. Running it locally means a full workspace build behind the shared verify lock; CI builds and runs it. This change adds no code snippet to any doc.

Lint — narrowing declared and measured. Ran eslint --no-inline-config on the two changed TS files instead of the repo-wide sweep. Exit 0, no output. The narrowing is a measurement, not a skip:

  1. Population came from eslint's own configuration, not from a guess about which files count — both paths appear in --format json output with a verdict rather than being ignored.
  2. File count read from --format json: 2 files, errors=0 warnings=0 each.
  3. Invariance for untouched files: eslint.config.js declares no parserOptions.project and no projectService, so type-aware linting is off and no rule reads across file boundaries. No lint config and no shared type consumed by a lint rule changed, so this diff cannot move the verdict of any file it does not contain.

Reverse verification — two ablations, each discriminating its own hunk

Both mutate a committed tree, confirm the mutation on disk by grepping the text meant to change (not an editor exit code, and not a bare git diff --stat), and restore under a trap ... EXIT INT TERM. The subject resolves by relative import (from '../recordActivityFeed'), so no dist and no rebuild is involved — stated because an ablation that silently ran against stale build output would stay green and prove nothing.

Ablation 1 — remove only scheduled: 'event',. Disk proof: injected text count 0, surrounding anchor completed: 'task', still 1. Direction predicted before running, and matched exactly — Tests 4 failed | 24 passed (28):

  • covers the declared vocabulary AND the values producers actually write
  • renders scheduled — it is stored by a real producer
  • maps a scheduled meeting row onto an event feed item
  • produces a feed type the spec actually declares

Non-discriminating for this hunk, named rather than counted as evidence: the three applyFeedConfig legs build an event item by hand to pin the pipeline and never touch the map; leaves every previously-mapped type pointing where it did normalises scheduled away on purpose, being the regression control for the other entries; the drop/warn legs use an unknown type, not scheduled.

Ablation 2 — remove only the diagnostic, leaving the map entry in place. Disk proof: call count 0, scheduled: 'event', still 1. Result Tests 2 failed | 26 passed (28) — the two legs asserting the warning, and no others, which is what shows the two probes are independent. Note the drop itself never regressed: still DROPS a type nothing maps went red on its console.warn assertion, not on its toBeNull(). stays SILENT for the types it deliberately drops stays green here by construction — it discriminates the opposite mutation (a warning that fires on deliberate drops).

Restore legs, both ablations: git diff HEAD --stat empty, marker text back at count 1, full re-run green.

Out of scope, filed unassigned


Generated by Claude Code

…event` (#5840)
`sys_activity` rows with `type: "scheduled"` had no entry in
ACTIVITY_TYPE_TO_FEED_TYPE, so they were discarded upstream of every filter a
page can configure and could never appear on any timeline. HotCRM's
`schedule_meeting` writes exactly that value for a not-yet-held meeting, on five
objects; the held branch (`completed`) reached the feed and the scheduled branch
did not.
Map `scheduled` to `event`, which is the semantic pairing and also makes the
declared `event` feed kind reachable for the first time.
The unknown-type default is deliberately unchanged: a type nothing maps is still
dropped, not bucketed into `system`, because rendering an unmeasured kind as
something it is not is new wrong data rather than recovered data. The drop stops
being silent instead — one `console.warn` per unknown type, and none for the
types the table knows and deliberately excludes.
The test's key-set pin used to assert set-equality with plugin-audit's declared
options. That premise is false: every field on `sys_activity` is readonly and
validateRecord skips readonly fields, so undeclared values are stored silently.
The pin now states both vocabularies — the declaration, plus the values a
shipped producer measurably writes, each naming its producer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3229.7 KB3990.2 KB
Main entry chunk (gzip)153.4 KB350 KB
Entry fileindex-CNszgRhv.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.13KB3.77KB
app-shell (runtime-config.js)13.57KB4.78KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)504.16KB114.08KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)186.91KB44.22KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.94KB61.30KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.80KB27.20KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.88KB1.85KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 17:42
@os-zhuang
os-zhuang added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit d8cf1cbAug 23, 2026
23 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5840-activity-scheduled-kind branch August 23, 2026 17:42
os-zhuang pushed a commit that referenced this pull request Aug 23, 2026
…s` filter (#5841)
`normalizeFeedTypes` returned `undefined` for three different authored inputs — no
`types` key, `types: []`, and a list whose every member was unrecognised — and
`applyFeedConfig` reads `undefined` as "apply no filter". So a page that named the wrong
feed kind was served EVERY activity on the record, with no diagnostic anywhere at
runtime. Measured in a real app: a lead page authored `types: ['crm_task']` (an object
name where a feed kind belongs) and its Activity tab rendered the audit stream for as
long as it shipped.
A sanitiser may narrow an author's request or refuse it; it must never silently widen
it. Widening turns a typo into "show the user everything" — the one outcome no author
asked for — and it hides behind a PLAUSIBLE result, since a populated timeline reads as
working while an empty one gets investigated.
`undefined` is now reserved for one meaning: no `types` key was authored. An authored
filter that keeps nothing returns `[]`, and the call site tests `!== undefined` rather
than truthiness, so `[]` filters to nothing. A non-array `types` is refused for the same
reason rather than ignored.
Unrecognised entries are named once, through the same warn-once plumbing #5886 added
for unmapped `sys_activity.type` values, now factored into a shared `warnOnce` with a
bucket per channel — the two vocabularies overlap, so one channel having spoken must not
silence the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

record:activity drops every sys_activity.type: "scheduled" row, and 10 of FeedItemType's 13 kinds have no producer at all

2 participants

@os-zhuang@claude