Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[finding] RecordDetailView hand-copies record:activity's sys_activity map, and nothing fails when the two disagree #5878

Closed

Description

@claude

Observation found while implementing #5840. Not fixed there: that card's fence is the record:activity renderer, and this file is a different package with its own suite. Filed so the divergence is visible rather than discovered later.

The duplicate

record:activity maps sys_activity.type onto feed kinds in
packages/plugin-detail/src/renderers/recordActivityFeed.ts (ACTIVITY_TYPE_TO_FEED_TYPE, exported).

packages/app-shell/src/views/RecordDetailView.tsx hand-copies the same table as a local activityTypeToFeed literal (near the sys_activity merge), rather than importing the exported one. The renderer's docblock states the two are meant to be the same reading — "Two renderers that disagree about what a created row IS would put the same record's history under two different icons depending on which block an author reached for."

They now disagree

#5840 adds scheduled -> event to the renderer's map, because a shipped producer (HotCRM's schedule_meeting) writes that value and the row was being dropped before any filter ran. The RecordDetailView copy was left untouched, deliberately and under the PM's fence.

Consequence today: a scheduled meeting appears on a hand-authored record page that uses the record:activity block, and is still dropped on the console record page, which uses the RecordDetailView merge. Same record, same row, two answers — exactly what the shared-map comment warns about.

The part that outlives this instance

Nothing fails when the two disagree. There is no test that compares them, and neither file imports the other, so any future edit to one silently reopens the gap. The fix is not "copy the line across"; it is that the second copy should read the exported map (or a test should pin them equal).

Upstream has recorded the same class of unguarded mirror from the other side: plugin-audit/src/objects/sys-activity-type-vocabulary.test.ts in objectstack notes that objectui pins its key set against a hardcoded literal rather than against the declaration, so an addition upstream does not reach it. It cites an issue number for that which returns 404 in objectstack today, so treat the reference as dangling rather than as prior work.


Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 23, 2026
  2. claude commented on Aug 23, 2026

    @claude
    ContributorAuthor

    Claiming this card.

    • session id: 01EuPCi56cnGyykygi3z9w4m
    • branch: claude/issue-5878-activity-map-single-reading

    All seats share the os-zhuang identity, so this comment (not the assignee field) is the claim of record. If you hold an earlier claim on this issue with a different session id, this one yields to yours.


    Generated by Claude Code

  3. added a commit that references this issue on Aug 23, 2026
    6ee1add
  4. claude commented on Aug 23, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 5878,
      "status": "done",
      "branch": "claude/issue-5878-activity-map-single-reading",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5894",
      "premise_still_valid": true,
      "summary": "Measured the dependency direction first, and it was not ambiguous: RecordDetailView.tsx ALREADY imports from @object-ui/plugin-detail on origin/main (line 13 -- RecordChatterPanel, InlineEditSaveBar, buildDefaultPageSchema, deriveFieldGroupDetailSections, extractMentions, resolveTitleField, useRecordEditable), app-shell declares plugin-detail in peerDependencies + devDependencies, and check-phantom-dependencies confirms the edge is declared. So shape (a): RecordDetailView imports ACTIVITY_TYPE_TO_FEED_TYPE and its hand-written literal is deleted -- no new dependency edge, no re-homing, no equality-pin stopgap. One enabling change was needed: the table was exported from renderers/recordActivityFeed.ts but NOT from the package entry point, so plugin-detail/src/index.tsx now re-exports it; that adds no module to the eager closure (index.tsx:24 already statically imports ./renderers/record-activity, which statically imports ./recordActivityFeed at its line 72, zero dynamic imports on the chain). recordActivityFeed.ts's membership is untouched. The pins carry the identity shape: a getter spy on the shared object plus a member injected into it at runtime -- a member-identical private copy fails both, where a value comparison passes ON the defect.",
      "tests": "Union re-run AFTER the final commit, on 6ee1addf6: `pnpm exec vitest run --maxWorkers=2` over the new pin file + RecordDetailView.feedRecordScope + RecordDetailView.feedLoading + recordActivityFeed.test.ts + record-activity.test.tsx => `Test Files  5 passed (5)` / `Tests  75 passed (75)`, UNION_VITEST_EXIT=0. The full app-shell suite was deliberately not run (~784s, holds the shared verify lock). New pin file alone, verbose: `Tests  7 passed (7)`. ABLATIONS -- no rebuild was needed and here is why: vitest.config.mts aliases @object-ui/plugin-detail to packages/plugin-detail/src and RecordDetailView is imported relatively, so no dist stands between the edit and the run; leg B going red is itself proof the mutation reached the executing code. Every leg confirmed ON DISK by grepping injected AND removed text (never an editor exit code), and the ablation script carries `trap ... EXIT INT TERM` so a cap kill cannot leave the tree mutated. Leg A (member-identical private copy, scheduled INCLUDED) -- disk check `injected_read_site=1 injected_literal=1 removed_read_site=0 private_scheduled_entry=1` -- predicted and observed `Tests  2 failed | 5 passed (7)`: both IDENTITY legs red (`the merge never read ACTIVITY_TYPE_TO_FEED_TYPE.scheduled - it is reading a copy: expected 0 to be greater than 0`; `Unable to find an element with the text: activity row of type os_probe_5878_not_a_real_activity_type`) while the `scheduled` BEHAVIOUR leg passed ON the defect -- which is the whole demonstration. Leg B (pre-#5840 copy, scheduled ABSENT, disk check `private_scheduled_entry=0`) -- `Tests  3 failed | 4 passed (7)`, SUBJECT red too, reproducing the shipped bug. Leg C, cross-package: app-shell/tsconfig.test.json sets `paths: {}` so plugin-detail resolves through the built dist/index.d.ts; a probe key the real type rejects produced `error TS1360: Type 'Readonly<Record<string, \"email\" | ... | \"event\" | ... | undefined>>' does not satisfy the expected type 'Record<string, number>'` -- TS1360 naming the real union, NOT TS2305 'has no exported member', which is what a stale declaration would have given. Tree restored after every leg, `git diff HEAD --stat` empty. NON-DISCRIMINATING legs, named: the 3 regression controls stayed green through both ablations by construction (a member-identical copy answers them identically) -- they are controls against a convergence that moved membership; `ACTIVITY_TYPE_TO_FEED_TYPE.scheduled === 'event'` is green in every leg by design; and the 'row rendered at all' assertion inside each identity leg is a LIVE control so a dead harness fails as a broken probe rather than reporting a fork. TYPE-CHECK after `pnpm --filter '@object-ui/app-shell^...' build` (29 packages): script name echoed in both logs, so a zero-match filter cannot read as green -- PLUGIN_DETAIL_TYPECHECK_EXIT=0, APP_SHELL_TYPECHECK_EXIT=0 (both run `tsc --noEmit && tsc -p tsconfig.test.json`). GATES, each quoted from its own verdict line, exit captured before any pipe: check-changeset-presence `2 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)` EXIT=0; check-changeset-no-major `No changeset declares a 'major' bump.` EXIT=0; check-control-bytes `OK (scanned 4902 tracked text file(s); skipped 85 binary)` EXIT=0; check-phantom-dependencies `Every in-scope import is declared by the package that publishes it.` EXIT=0 (decisive here -- it is what confirms importing a value from a peerDependency is declared); check-package-self-import EXIT=0; check-spec-symbol-derivation EXIT=0; check-i18n-call-site-keys EXIT=0; check-node-esm-load --specifiers-only EXIT=0. Also self-scanned the touched files with `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` -- clean. DECLARED NARROWING (lint): ran eslint over the 3 touched files, EXIT=0, errorCount 0, 147 warnings. All three pieces of evidence: (1) population read from eslint's OWN config via its isPathIgnored -- 3524 tracked .ts/.tsx in scope, 0 ignored; (2) files linted read from --format json -- 3; (3) invariance -- type-aware linting is NOT enabled (root flat config uses tseslint.configs.recommended, languageOptions declares no parserOptions.project/projectService), so no untouched file's verdict can move. The warning count was measured against the base rather than assumed: linting origin/main's copies gives index.tsx 28 before / 28 after and RecordDetailView.tsx 113 before / 113 after -- the re-export added none; the only new warnings are 6 no-explicit-any in the new test file, matching the sibling harness's fake-dataSource convention. DECLARED NOT-RUN (not a pass): check-eager-closure-budget exits 2, its documented 'broken gauge, not a passing budget' code, because it reads apps/console/dist/eager-closure.json which only a console vite build writes; producing it locally needs app-shell + plugin-gantt/-markdown/-timeline dists outside app-shell's dependency closure and the attempt failed on TS2307 for exactly those, unrelated to this diff. performance-budget.yml builds the console and runs it on this PR; the specific claim it would check is supported statically above. CI status at report time: in_progress -- per the dispatch contract the report lands at draft-PR time and CI convergence is the PM's read.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5896: the sys_activity row -> FeedItem CONSTRUCTION is still written twice (activityRowToFeedItem + activityTimestamp vs RecordDetailView's inline loop) -- three measured divergences already: unmapped types drop silently on the console surface where the block warns once, a different missing-timestamp fallback (String(created_at ?? '') vs a possibly-undefined assignment), and two independent i18n lookups for the system-actor label. Deliberately not folded into this PR: adopting activityRowToFeedItem changes observable behaviour (a new console.warn on that surface, a changed timestamp fallback), so it needs its own tests and its own read -- doing it here would have widened the verification surface of a card whose thesis was 'one table, one reading'. Unassigned, labelled finding + domain:ui, no pm:queue."
      ]
    }

    Generated by Claude Code

  5. added 2 commits that reference this issue on Sep 1, 2026
    d06fba8
    dbd5194
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfinding

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions