Repository navigation
[finding] 8 of FeedItemType's 13 kinds still have no producer on any objectui surface #5877
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 23, 2026 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/specenum + 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/sharingas 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 onrecordActivityFeed'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 tosys_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
- ⛔ Directions 1 (build producers) and 2 (narrow the published enum) are not in this card. Both change capability surface; 2 is spec-side (
Census correction —
systemis no longer a "no producer" candidate, and this card is about to become a single-site changedomain:uiexecution seat, sessionsession_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.
⚠️ systemhas a producer now — from every author-extended typePR #6112 gave
activityRowToFeedItema defined fallback,UNMAPPED_ACTIVITY_FEED_TYPE, which is'system', for anysys_activity.typethe mapping table has never heard of. #6731 makes the console surface call that same constructor instead of its own copy.⇒
systemis now reachable from any author-extended activity type on both surfaces. If this card's "8 of 13 kinds have no producer" census countssystemamong 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(therecord:activityblock andRecordDetailView's inline copy), which had already drifted into different behaviours. After it, both reachFeedItemTypethrough 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'smapActivityRowsis a third hand-written reading ofsys_activity.type, targetingActivityItem— a different, four-value vocabulary — and it buckets every unrecognised type, author-extended values included, asupdate.It drops nothing, so it is drift risk rather than a live defect. But it means "one constructor" is true for
FeedItemTypeand not for the activity vocabulary as a whole, and #6730 records that converging it needs theActivityItemvsFeedItemquestion answered first.⇒ A census of
FeedItemTypeproducers should say explicitly whether it is counting that surface, since it produces a different type from the same rows.Status
This card stays
pm:queueand 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
File-face re-verification on the merged ref — done, and it changes where half of this card belongs
domain:uiexecution seat, sessionsession_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:13now importsactivityRowToFeedItemfrom@object-ui/plugin-detail; the inline copy is gone. Both surfaces reachFeedItemTypethrough one constructor, so a decision here is a single-site change. - ✅
UNMAPPED_ACTIVITY_FEED_TYPEis'system'(recordActivityFeed.ts:162), reachable from any author-extended type. As the earlier comment recorded:systemis not a "no producer" candidate. ⚠️ The third reading is still there.packages/app-shell/src/hooks/sharedUserFeeds.tsstill carriesmapActivityRows, targetingActivityItem— a different four-value vocabulary. Tracked as [finding] sharedUserFeeds keeps a THIRD hand-written sys_activity.type reading, and buckets every unrecognised type asupdate#6730. So "one constructor" is true forFeedItemTypeand not for the activity vocabulary as a whole.
⚠️ The routing fact that matters more than any of the aboveFeedItemTypeis 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/specand re-exported.views.ts:338even records that it andFeedFilterModewere deliberately kept as live activity vocabulary.⇒ This card has two halves with different owners:
- The census — measuring which kinds have a producer on an objectui surface — is
domain:uiwork and is what this card actually measured. - 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/specgoes to thedomain:specseat 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:speccard inobjectstackwith this measurement attached and aBlocked-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:systemhas moved out of the no-producer set, and I have not re-counted the other seven.The card stays
pm:queueand unassigned; the hold that was on it is released.
Generated by Claude Code
- ✅ The convergence is real.
claude commented
on Aug 31, 2026 claudeboton Aug 31, 2026 – with ClaudeContributorAuthorMore actionsos-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
Split out of #5840 by PM ruling: that card's fix extends the
sys_activitytype map; narrowingFeedItemTypeis 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:Producers that exist on any objectui surface today (after #5840 lands
scheduled->event):field_changesys_activitytypes created/updated/deleted/assigned/shared, viapackages/plugin-detail/src/renderers/recordActivityFeed.tstasksys_activitytypecompletedsystemsys_activitytypesystemeventsys_activitytypescheduled— new in #5840commentsys_commentrows, mapped inpackages/app-shell/src/views/RecordDetailView.tsx(the host discussion-context path, not thesys_activitymap)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:
commentdoes have a producer (it arrives on the host feed rather than through the activity map), andeventgains 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/sharingare NOT used becauseRecordDetailViewand the block must agree about what acreatedrow 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:
typesentries 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