Skip to content

fix(app-shell): one reading for sys_activity.type, and the unrecognised case stops claiming update - #6814

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6730-shared-user-feeds-activity-type
Aug 30, 2026
Merged

fix(app-shell): one reading for sys_activity.type, and the unrecognised case stops claiming update#6814
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6730-shared-user-feeds-activity-type

Conversation

@claude

@claudeclaudeBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Fixes#6730

A drift card, answered in the order the card asks: first what the right shared
shape is, then the one thing that was a defect regardless.

1. The architecture question: the two vocabularies CROSS

The card warns that this is not simply "adopt activityRowToFeedItem". Measured
against ACTIVITY_TYPE_TO_FEED_TYPE as it stands today, it is worse than that —
the two readings are lossy in both directions, so neither type is a
projection of the other:

FeedItem (plugin-detail)ActivityItem (app-shell)
created / updated / deleted / assigned / sharedone field_changesplit three ways: create / update / delete
commented / mentionedundefined — deliberately dropped, content lives in sys_commentthe comment kind, one of four
system / completed / scheduledthree feed types (system / task / event)one bucket

Routing this surface through the shared constructor would therefore cost the
bell every comment row
(activityRowToFeedItem returns null for them) and
every create/delete distinction (field_change cannot be decomposed). A
shared reading would have to be a third table keyed on sys_activity.type with
two value columns — which is not a convergence, it is the same two decisions
written next to each other, plus a runtime edge from the shell's header chrome
onto a record-detail widget plugin (@object-ui/plugin-detail is a peer
dependency of @object-ui/app-shell, so that edge is a real install-time
requirement).

So the two readings stay separate — and what is shared is a pin, not an
import.
activityItemType-6730.test.ts reads plugin-detail's real table via
the devDependency (no runtime edge) and fails when:

  • the column's declared vocabulary grows an entry this side has not read
    (map ⊇ built-ins, the one-directional shape objectui#5969 landed on — the
    column is author-extensible, so an equality pin would be false by
    construction); or
  • the two readings stop disagreeing in the three ways above, i.e. somebody makes
    convergence viable. That is then a decision, taken against a red test that
    says what changed.

Before this PR nothing red-flagged the disagreement, which is exactly how
scheduled reached the console record page after objectui#5878 and never
reached the bell.

2. The silent default — measured, and what it actually costs today

Today every value outside the four named ones renders as update. That is not a
missing decision, it is a wrong one stated out loud: a scheduled meeting, a
login, a nightly system rollup and an author's contract_countersigned all
claim "somebody updated this record".

What the reader deserves to know is how visible that is. Measured on
origin/main:

  • InboxPopover's Activity tab (layout/InboxPopover.tsx:545) renders
    a.user, a.description, a.timestamp and a.objectNamenever
    a.type.
  • HomeActivity (console/home/HomeRail.tsx:262) renders the same four —
    nevera.type.
  • The only reader of ActivityItem['type'] is the ActivityFeed Sheet, which
    is exported from the package barrel and has zero in-repo call sites. There
    the type drives the icon, the colour, the badge label and the
    notification-preference filter — and that filter is a
    Record(ActivityItemType, boolean) lookup, so a kind missing from the record
    is silently filtered OUT of the panel.

So the wrong bucket is invisible on both surfaces the card names, and visible
only on a published component this repo never mounts. That is why nothing was
ever observed — and it is also why this change cannot regress the bell or Home:
they do not read the field. It does not make the defect unreal; the union and
the component are both published API.

3. What changed

  • New layout/activityItemType.ts — the whole reading in one place: the
    table, the generic bucket, the "NOW()" timestamp fallback and the row
    constructor that applies all three. DOM-free, so what a row becomes is
    assertable without mounting a Sheet (recordActivityFeed.ts's own precedent
    on the other side). Exported as the whole reading rather than its pieces, for
    the reason objectui#5896 gave: publishing the table alone left the mirror one
    level up, and the constructions drifted where the tables did not.
  • ActivityItem['type'] gains a fifth kind, system — the generic bucket.
    ActivityFeed keeps three Record(ActivityItemType, …) tables, so tsc
    refused the member until it had an icon, a label and a default notification
    toggle. That compile-enforcement is what makes the bucket safe to add.
  • Built-ins that had no honest presentation among the four existing kinds now
    land in the bucket instead of claiming update
    : system, completed,
    scheduled, login, logout. assigned and shared stay update — both
    write to the record, which is also why they sit in field_change on the other
    side. The stated rule: create / update / delete mean the record's own
    stored state changed
    , split by which way.
  • An unrecognised value renders through the bucket and is named once on
    console.warn, following UNMAPPED_ACTIVITY_FEED_TYPE's precedent — a bucket,
    not a drop, because sys_activity.type is author-extensible
    (objectstack#11507 direction 4, ruled 2026-08-24) and dropping is
    objectui#5840's failure mode. A value the table maps tosystem on purpose
    stays silent, which is why the lookup asks hasOwnProperty rather than
    comparing its result to the bucket.
  • The "NOW()" quirk stays a copy, deliberately, and is now pinned. It is
    the only target-type-independent part of the reading, so it is the one thing
    that genuinely could be shared — but activityTimestamp is not on
    plugin-detail's barrel (objectui#5896 published the whole FeedItem reading on
    purpose, not its pieces), and importing it would buy one five-line predicate
    for a peer-dependency edge on the header chrome. There is no package that owns
    "how to read a sys_activity column" for both a widget plugin and the shell.
    Until there is, the honest instrument is a pin: the suite asserts the two
    copies agree value for value over the quirk's whole input table.
  • 10 locale packs gain layout.activityFeed.typeSystem.

Scope held: nothing under record:activity or RecordDetailView is touched —
objectui#5878 and objectui#5896 own those. The plugin-detail import added is
test-only.

4. Reverse verification (direction predicted BEFORE running)

Predicted: restoring the pre-#6730 inline reading in mapActivityRowsonly
turns the e2e suite RED while the pure suite stays GREEN — because the pure
suite's subject is the table, and the defect was never in a table, it was in the
reading done inline around it.

Measured, mutation and restore both confirmed on disk by blob hash (script kept
a trap restore; git diff HEAD empty afterwards, restored blob identical to
the HEAD blob 687fd0125519ba7c436a2db8b1f3d7c015eb9183):

on disk: old catch-all lines=1 new-reading references=0
mutated blob: b98ea22867c00959cfc27334d5247f2fcb6098f1
{
"r1": "create", "r2": "update", "r3": "delete", "r4": "comment",
- "r5": "system", "r6": "system", "r7": "system",
+ "r5": "update", "r6": "update", "r7": "update",
}
Test Files 1 failed | 1 passed (2)
Tests 2 failed | 14 passed (16)

r5 is scheduled, r6 is an author-extended contract_countersigned, r7
is system. Two directions worth naming:

  • The pure suite stayed green under the mutation. That asymmetry is why both
    files exist: a pin on a table the producer does not call is objectui#5896's
    failure mode.
  • The "NOW()" case stayed green in both legs, which is the measurement the
    card asks for: the fold is behaviour-preserving on this surface — the pin
    cannot tell the two implementations apart, which is the whole point of it.

No rebuild was needed for either leg, and this is a property of the harness
rather than an assumption: vitest.config.mts:289 aliases
@object-ui/plugin-detail to packages/plugin-detail/src, and
sharedUserFeeds.ts is reached by relative path — no dist participates in
these runs.

And the honest drift-card answer to "does anything fail when you revert?":
before this PR, nothing did.
No test in the repo referenced mapActivityRows
or pinned any part of this mapping; reverting the whole change with only the
pre-existing suites green is a no-op. That absence is the finding the card was
filed for, and closing it is what the two new suites are.

5. Verification

All heavy runs serialized through the shared verify lock; verdicts quoted from
the lock's own VERDICT line, not from a piped $?.

  • pnpm --filter '@object-ui/app-shell^...' buildVERDICT command-exit 0
  • pnpm --filter @object-ui/app-shell type-check && pnpm --filter @object-ui/i18n type-check
    VERDICT command-exit 0. app-shell's type-check is
    tsc --noEmit && tsc -p tsconfig.test.json, so the new suites are covered by
    it (they were not, at first — three errors, fixed in the second commit).
  • new suites: 16 passed (2 files).
  • pnpm exec vitest run --maxWorkers=2 packages/app-shell/ packages/i18n/ at
    e3cb0dbcc (the final commit) — VERDICT command-exit 0, Test Files 634 passed (634),
    Tests 6573 passed | 1 skipped (6574).
  • Gates re-derived from the actual diff, all EXIT=0: check:control-bytes,
    check:i18n-keys, check:i18n-drift, check:phantom-deps,
    check:self-import, check:vi-mock-specifiers, changeset:check.
  • check:readme-exports and check:eager-closure are NOT MEASURED locally,
    not red: both say so themselves — the first needs pnpm build for six
    packages this diff never touches (app-shell's own entries pass now that it is
    built), the second needs an apps/console build and prints "This is a broken
    gauge, not a passing budget." CI runs both properly.

Lint was narrowed to the changed files, and the narrowing is declared, with
its three pieces of evidence.
(1) The population is eslint's own — the run used
--no-inline-config, the same flag the root lint script uses, and it is
stricter than the per-package eslint . CI actually runs. (2) File count read
from --format json: 6 files, 0 errors, 0 warnings. (3) Invariance for
untouched files: eslint.config.js sets no project / projectService and
extends tseslint.configs.recommended (not recommendedTypeChecked), so no
rule reads another file's types
and this diff cannot move a verdict on a file
it does not touch; the diff also removes no export, so no importer's resolution
changes. CI runs the full farm regardless.


Generated by Claude Code

…ed case stops claiming update
`mapActivityRows` (hooks/sharedUserFeeds.ts) carried the third hand-written
reading of `sys_activity.type` in this repo and bucketed every unrecognised
value as `update` — a specific, often wrong claim about the row. #5878 shared
the table between `record:activity` and `RecordDetailView`, #5896 shared the
constructor; this copy survived both, including its own `"NOW()"` timestamp
fallback.
The two readings are deliberately NOT converged: `activityRowToFeedItem` builds
a `FeedItem`, whose vocabulary CROSSES this one — it collapses
create/update/delete into a single `field_change` and drops `commented` /
`mentioned` outright, so routing this surface through it would cost the bell
every comment row and every create/delete distinction. What is shared instead
is a pin, not an import.
- new `layout/activityItemType.ts`: the whole reading (table, generic bucket,
`"NOW()"` fallback, row constructor), DOM-free so it is directly assertable
- `ActivityItem['type']` gains `system`, the generic bucket; `tsc` forces the
icon, label and notification toggle for it in `ActivityFeed`
- `system` / `completed` / `scheduled` / `login` / `logout` land in the bucket
instead of claiming `update`; unrecognised values render through it and are
named once, following `UNMAPPED_ACTIVITY_FEED_TYPE`'s precedent
- `activityItemType-6730.test.ts` reads plugin-detail's real table
(devDependency, no runtime edge) and fails when the declared vocabulary grows
an entry this side has not read, or when the two readings stop disagreeing
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
`tsc -p tsconfig.test.json` covers test files in this package; the timestamp
equivalence table now declares `SysActivityRow` (the shape the folded copy
accepts) and the e2e suite collects warnings into a typed array rather than
reaching into an untyped spy handle.
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

MetricValueBudget
Eager closure (gzip, 45 chunks)3174.6 KB3222.7 KB
Main entry chunk (gzip)148.7 KB350 KB
Entry fileindex-BVoZnRrL.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)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
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)511.50KB116.32KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)240.93KB60.76KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
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.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.46KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.03KB32.64KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.57KB54.55KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.87KB21.12KB
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)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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 (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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

@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

Contract review PASS at head e3cb0dbcc — director seat (session session_01DxbNgzPMo4YuRBmGmCQp9m, tier claude-fable-5), acting on the maintainer's direct instruction (2026-08-30, verbatim: 「这几个objectui pr 也帮我检查下,没问题就合并」).

Full-diff review (16 files, +775/−45): ① union widening ActivityItem['type'] + system is tsc-enforced complete — all three Record<ActivityItemType,…> tables extended (icon/label/toggle), 10 locale packs carry typeSystem, re-export keeps every existing import path resolving; ② activityItemTypeOf correctly distinguishes mapped-system (silent, a decision) from unmapped (warn once per value) via hasOwnProperty; ③ activityRowToActivityItem preserves the old inline filter semantics exactly (string type + non-blank summary), and activityRowTimestamp is value-equivalent to the old inline quirk handling; ④ the deliberate NON-convergence with activityRowToFeedItem is correctly argued (vocabularies cross in three measured ways) and pinned bidirectionally — superset pin one-directional per the author-extensible column ruling (objectstack#11507 d4); ⑤ reverse verification ran with direction predicted first, mutation/restore proven by blob hash; ⑥ CI 30/30 green at this head. Changeset grading (app-shell minor / i18n patch) is right for a published-union widening.

Flipping ready + arming auto-merge (queue). Verdict recorded here on the PR per the #13417 visibility rule.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review August 30, 2026 13:49
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit dbd5194Aug 30, 2026
32 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-6730-shared-user-feeds-activity-type branch August 30, 2026 14:02
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.

[finding] sharedUserFeeds keeps a THIRD hand-written sys_activity.type reading, and buckets every unrecognised type as update

2 participants

@zhuangjianguo@claude