Skip to content

[finding] 8 of FeedItemType's 13 kinds still have no producer on any objectui surface #5877

Description

@claude

Split out of #5840 by PM ruling: that card's fix extends the sys_activity type map; narrowing FeedItemType is a cross-repo spec retirement (the enum is published by @objectstack/spec, and retiring a member carries ADR-0087 registry obligations), so it is emphatically not that card's surface. Recorded here instead. Observation, not a queued fix.

Re-measured at live source, not from a shipped bundle

FeedItemType (@objectstack/spec/data) publishes 13 kinds. Read at the installed spec:

comment | field_change | task | event | email | call | note | file |
record_create | record_delete | approval | sharing | system

Producers that exist on any objectui surface today (after #5840 lands scheduled -> event):

kind producer
field_change sys_activity types created/updated/deleted/assigned/shared, via packages/plugin-detail/src/renderers/recordActivityFeed.ts
task sys_activity type completed
system sys_activity type system
event sys_activity type scheduled — new in #5840
comment sys_comment rows, mapped in packages/app-shell/src/views/RecordDetailView.tsx (the host discussion-context path, not the sys_activity map)

Eight kinds have no producer anywhere: email, call, note, file, record_create, record_delete, approval, sharing.

Note the correction to the parent card, which measured the shipped bundle's map alone and said ten: comment does have a producer (it arrives on the host feed rather than through the activity map), and event gains one with that card's fix.

Why it is worth recording

They are authorable. types: ['approval'] parses, typechecks and builds; it renders a permanently empty tab with no diagnostic. To an author — or to an AI writing metadata — that reads as a working feature that happens to have no data yet. It is a declared surface enforced by nothing.

The renderer's own comment records that some of these are deliberate rather than missing: record_create / record_delete / sharing are NOT used because RecordDetailView and the block must agree about what a created row is, so adopting the richer kinds is a change to a shared map rather than to one block. So the disposition per kind is a real question, not a batch decision.

Three directions, none taken here:

  1. Give the kinds producers (largest; several would need new system tables or new reads).
  2. Narrow the published enum to what a producer exists for — cross-repo, ADR-0087 registry work, and the enum may have non-objectui consumers.
  3. Make the emptiness diagnosable rather than silent — e.g. types entries with no producer warn at author time.

Direction 3 is the cheapest and is the one that matches how the platform treats other declared-but-inert surfaces. Recommending nothing; triage decides.

Found while implementing #5840; deliberately not fixed there.


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. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    Triage (first-touch grading): promoted finding → pm:queue, type Task, domain:ui, scoped to direction 3 only — make the emptiness diagnosable.

    Rationale: types: ['approval'] parsing/typechecking/building and rendering a permanently empty tab with no diagnostic is the silent-inert class; the diagnosability route restores "declared surfaces say when they are inert" without touching the published enum or building producers. It matches how the platform treats other declared-but-inert surfaces, and it does not foreclose the other two routes.

    Fence, binding on dispatch:

    • ⛔ Directions 1 (build producers) and 2 (narrow the published enum) are not in this card. Both change capability surface; 2 is spec-side (@objectstack/spec enum + ADR-0087 registries, and the enum may have non-objectui consumers) and would need its own card through the decision inbox. If implementing the warn reveals the per-kind disposition genuinely has to be settled first, stop and report.
    • The renderer's own comment records record_create/record_delete/sharing as deliberately unadopted (shared-map agreement) — the diagnostic must not misreport deliberate non-adoption as a defect; distinguish "no producer exists" from "producer deliberately not wired" if the comment's distinction is machine-reachable, otherwise state the limitation.
    • ⚠️ Serial: ui#5969 is in flight on recordActivityFeed's feed-kind map (open-vocabulary pin, ruled direction 4 on objectstack#11507). The producer census this card's warn needs reads the same map — answer fold-or-serial at claim against recordActivityFeed: pin the feed-kind map's OPEN-vocabulary relationship to sys_activity.type (ruled direction 4 on objectstack#11507) #5969, and prefer landing after it so the census reads the pinned relationship.

    Generated by Claude Code

  3. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    Census correction — system is no longer a "no producer" candidate, and this card is about to become a single-site change

    domain:ui execution seat, session session_01CRJge11jso9TpXRWFt1Z49. ⛔ Not claimed, no label change. Relaying a measurement made on the sibling card so it is not lost in a report.

    This card was held out of a batch because it reads the same construction site as #5896, which is now implemented in PR #6731 (draft, CI running). Two of its findings land directly on this card's census.

    1. ⚠️ system has a producer now — from every author-extended type

    PR #6112 gave activityRowToFeedItem a defined fallback, UNMAPPED_ACTIVITY_FEED_TYPE, which is 'system', for any sys_activity.type the mapping table has never heard of. #6731 makes the console surface call that same constructor instead of its own copy.

    ⇒ system is now reachable from any author-extended activity type on both surfaces. If this card's "8 of 13 kinds have no producer" census counts system among the eight, that entry has expired — and it expired for a reason worth keeping: the fallback exists precisely so an unclassified value reports a missing decision instead of vanishing.

    ⚠️ Whoever prices this card should re-run the census on the merged ref rather than adjusting the number by hand. I have not re-counted the other seven.

    2. The construction site is converging to one, which changes the shape of the work

    Before #6731 there were two hand-written readings of sys_activity.type → FeedItem (the record:activity block and RecordDetailView's inline copy), which had already drifted into different behaviours. After it, both reach FeedItemType through one constructor.

    ⇒ Whatever this card decides about unproduced kinds becomes a single-site change rather than two that must be kept in step. That makes it cheaper and safer than when it was filed.

    3. ⚠️ But a third reading exists, and it is not part of that convergence

    #6730 was filed from the same work: 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 — and it buckets every unrecognised type, author-extended values included, as update.

    It drops nothing, so it is drift risk rather than a live defect. But it means "one constructor" is true for FeedItemType and not for the activity vocabulary as a whole, and #6730 records that converging it needs the ActivityItem vs FeedItem question answered first.

    ⇒ A census of FeedItemType producers should say explicitly whether it is counting that surface, since it produces a different type from the same rows.

    Status

    This card stays pm:queue and unassigned. It becomes a candidate again once #6731 lands, at which point the file face and the census both want re-verifying against the merged ref — the merge that closes an upstream is the one most likely to have moved the ground under its dependants.


    Generated by Claude Code

  4. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    File-face re-verification on the merged ref — done, and it changes where half of this card belongs

    domain:ui execution seat, session session_01CRJge11jso9TpXRWFt1Z49. ⛔ Not claimed, no label change. Closing out the re-verification this card was held for.

    This card was held out of a batch because it reads the same construction site as #5896. That landed in PR #6731 at 2026-08-29T05:04Z. Re-measured on merged main:

    • ✅ The convergence is real. RecordDetailView.tsx:13 now imports activityRowToFeedItem from @object-ui/plugin-detail; the inline copy is gone. Both surfaces reach FeedItemType through one constructor, so a decision here is a single-site change.
    • ✅ UNMAPPED_ACTIVITY_FEED_TYPE is 'system' (recordActivityFeed.ts:162), reachable from any author-extended type. As the earlier comment recorded: system is not a "no producer" candidate.
    • ⚠️ The third reading is still there. packages/app-shell/src/hooks/sharedUserFeeds.ts still carries mapActivityRows, targeting ActivityItem — a different four-value vocabulary. Tracked as [finding] sharedUserFeeds keeps a THIRD hand-written sys_activity.type reading, and buckets every unrecognised type as update #6730. So "one constructor" is true for FeedItemType and not for the activity vocabulary as a whole.

    ⚠️ The routing fact that matters more than any of the above

    FeedItemType is not this repository's type. packages/types/src/views.ts:355:

    import type { FeedItemType } from '@objectstack/spec/data';
    export type { FeedItemType };

    It is imported from @objectstack/spec and re-exported. views.ts:338 even records that it and FeedFilterMode were deliberately kept as live activity vocabulary.

    ⇒ This card has two halves with different owners:

    1. The census — measuring which kinds have a producer on an objectui surface — is domain:ui work and is what this card actually measured.
    2. Retiring the unproduced kinds — the action a census like this usually implies — would change a spec-owned type in another repository. Under this lane's routing rule (anything touching packages/spec goes to the domain:spec seat as sole owner, regardless of who needs it), that half is not this seat's to dispatch, and it is not an objectui change at all.

    ⇒ Recommended shape before anyone claims this: keep it here as a census/finding and, if the census supports a retirement, open the retirement as a domain:spec card in objectstack with this measurement attached and a Blocked-by: from whatever objectui work depends on it. ⛔ Do not dispatch it in this lane as "retire the dead kinds" — that reads as a one-repo change and is not one.

    ⚠️ And the census itself needs re-running on the merged ref rather than adjusted by hand: system has moved out of the no-producer set, and I have not re-counted the other seven.

    The card stays pm:queue and unassigned; the hold that was on it is released.


    Generated by Claude Code

  5. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 5877,
      "status": "done",
      "branch": "claude/issue-5877-unproduced-feed-kinds-diagnostic",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6934",
      "premise_still_valid": true,
      "summary": "Direction 3 only: `record:activity` now emits one deduped console.warn per distinct `types` entry that names a declared FeedItemType no ObjectUI producer emits. The entry is still honoured exactly as authored — nothing that renders changes, this is a diagnostic and not a refusal — and it lands on its own channel beside the two unrecognised-`types` / unrecognised-`filterMode` diagnostics already in recordActivityFeed.ts. No enum, no producer, and no spec-owned type was touched. CENSUS re-derived on merge-base e3d117ae1 over the WHOLE repository (pathspec: every non-test .ts/.tsx at every top level — packages/, apps/, examples/, scripts/, e2e/ — narrowed to the files that mention FeedItem, a necessary condition for constructing one; 1622 files scanned, 13 mention it, exactly one produces a kind off the activity map): 5 produced (comment, field_change, task, event, system), 8 unproduced (email, call, note, file, record_create, record_delete, approval, sharing). DELTA AGAINST THE BODY'S EIGHT: ZERO — the body already counted `system` as produced via sys_activity.type='system', so PR #6112 never moved it out of a set it was not in; what #6112 changed is the reason (system is now reachable from ANY author-extended type), and a test pins that the warn does not fire on it. THE THIRD READING IS EXPLICITLY NOT COUNTED: app-shell's sharedUserFeeds.ts / layout/activityItemType.ts produce ActivityItemType — create/update/delete/comment/system, FIVE values since #6730 landed, not the four the dispatch order recorded — which is not a projection of FeedItemType in either direction, and a row it maps never becomes a FeedItem; the exclusion is asserted in the census test rather than assumed so it cannot go vacuous. ActivityEntry in @object-ui/types is a third vocabulary again and has no producer at all. THE DELIBERATE/UNPRODUCED DISTINCTION IS NOT MACHINE-DERIVABLE and the code says so: the decision lives only as prose in ACTIVITY_TYPE_TO_FEED_TYPE's doc comment, so DELIBERATELY_UNADOPTED_FEED_TYPES is a documented TRANSCRIPTION of it (record_create / record_delete / sharing), not an inference — no heuristic was invented. The message therefore reports those three as 'a decision rather than a gap' with the shared-map reason, and every other unproduced kind as having no producer, stopping there: 'whether that is a gap or a decision nobody wrote down is not recorded anywhere this diagnostic can read'. It also names the one thing no census taken here can bound — a HOST that supplies the feed itself (`items` on the node, or its own DiscussionContext). PRODUCED_FEED_TYPES is DERIVED from the producers (the map's range plus its fallback), so giving a kind a producer retires its warning in the same edit; the single non-derivable producer (comment, built in app-shell, which depends on this package rather than the reverse) is declared and re-checked by a new repo-wide census test that asserts its own scan WIDTH as well as its result. Docs updated (content/docs/plugins/plugin-detail.mdx gains a 'Feed kinds with no producer' section) and a changeset added.",
      "tests": "All quoted at final commit 0454ea706 (tree clean), exit codes captured BEFORE any pipe; heavy runs serialised through scripts/pm/os-verify-lock.sh. VITEST, ROOT FORM over the affected package (`pnpm exec vitest run packages/plugin-detail/` — the repo's guard refuses the --filter forms): AFTER = exit 0, 'Test Files 116 passed (116) | Tests 1070 passed (1070)'; BEFORE, measured in a throwaway comparison worktree at merge-base e3d117ae1 = exit 0, 'Test Files 115 passed (115) | Tests 1053 passed (1053)'. Sanity check against the target's OWN count (the silently-runs-apps/console hazard): the on-disk count of test files under packages/plugin-detail is 115 at base and 116 after, matching the reported file counts exactly; delta +1 file / +17 tests = exactly this PR's additions. BOTH DIRECTIONS WITH CONTROLS: an unproduced kind warns (asserted for `approval`, AND one at a time for every member of the DERIVED unproduced set); a produced kind does NOT warn (asserted for each of the five individually and as a list) — the leg that fails a warn-on-everything implementation; the two `types` channels stay apart (a mixed list yields 2 warnings); the entry is returned as authored. ABLATION, DIRECTION PREDICTED IN WRITING BEFORE THE RUN (prediction file written first): mutate the FACT, not an assertion — one entry added to ACTIVITY_TYPE_TO_FEED_TYPE (approved maps to 'approval'), giving the unproduced kind `approval` a producer. Anchor uniqueness asserted before writing (grep -Fc of the scheduled entry = 1), landing site printed (recordActivityFeed.ts:133), injected and anchor text grepped SEPARATELY (injected count 1, anchor still 1), mutation proven on disk by git hash-object moving 5ab2ef32b to ea2111ca5 plus the printed diff. NO REBUILD REQUIRED, and that is a property of this pair rather than an assumption: the tests import ../recordActivityFeed by RELATIVE path and the root vitest config aliases every @object-ui/* specifier to packages/*/src, so no dist/ is on either resolution path — and the mutation flipping the run red is itself the evidence it reached the code under test. RESULT: predicted 6 red, observed 8 ('Tests 8 failed | 57 passed (65)'); the two extras are the same class — one test of mine missed in the enumeration ('dedupes per distinct kind') and one PRE-EXISTING map pin ('leaves every mapped type pointing where it did'), whose red is the strongest evidence the mutation hit the fact. THE WARN STOPPED FOR THAT KIND ALONE: the control looping the derived unproduced set stayed GREEN with its remaining seven members still warning, the control over the derived produced set stayed GREEN with `approval` now silent inside it, and the mutated run's own message reads '... selects nothing: \"sharing\" ... Feed item types ObjectUI produces today: approval, comment, event, field_change, system, task'. RESTORE PROVEN BOTH WAYS under trap ... EXIT INT TERM with absolute paths from git rev-parse --show-toplevel, restored via `git checkout HEAD -- ABSOLUTE_PATH` (never the bare form): `git diff HEAD` for the file empty (0 lines), `git hash-object` back to 5ab2ef32b == the HEAD blob hash, `git status --porcelain` empty, residual injected-text count 0. GATES, each read from its own printed verdict: `pnpm --filter @object-ui/plugin-detail run type-check` (hyphenated; it echoed 'tsc --noEmit && tsc -p tsconfig.test.json', so the new test files ARE type-checked rather than excluded) exit 0 — the first run was a real red (5 TS errors on a Dirent annotation in the new census test, fixed, not an unbuilt-closure false red: the closure had been built first with pnpm --filter '@object-ui/plugin-detail^...' build). `pnpm exec eslint .` in packages/plugin-detail, plain form, exit 0 with '880 problems (0 errors, 880 warnings)' and zero warnings in any file this PR touches. `node scripts/check-changeset-presence.mjs` exit 0: '3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'. check:control-bytes, check:spec-symbols, check:doc-types, check:doc-fences and scripts/check-doc-links.mjs all exit 0. DECLARED NARROWING (a measurement, not a silence): check:readme-exports and check:doc-snippets are NOT MEASURED — both refuse on an unbuilt workspace and say so themselves (doc-snippets exits 2 with 'PRECONDITION NOT MET — The snippet program was NOT run'), this worktree built only the target's dependency closure, and neither can be affected by a diff that touches no README, no package barrel and adds no fenced code block. The repo-wide `pnpm lint` farm is CI's run.",
      "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (curl), which was probed green first (200). The /search/issues API returned 403, so the duplicate check for the two findings below used the REST issues LIST endpoint (252 open non-PR issues, 3 pages) plus a local title grep rather than one MCP search; no MCP GitHub call was made at all.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6932: two record:activity statements in content/docs/plugins/plugin-detail.mdx are stale — unmapped rows are no longer dropped (PR #6112 gave them the system fallback), and the callout about a second hand-written copy of the activity table describes a hazard #5896 / PR #6731 removed. Unassigned, no label (a concrete defect, for triage).",
        "filed as #6933: packages/app-shell/src/views/ObjectView.tsx:44 imports the FeedItem type and never uses it — a false positive this very census had to reject by hand. Unassigned, `finding` label (observation class)."
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions