Skip to content

console: an author cannot edit or delete their own comment on a record page, though sys_comment's data layer allows both (split from objectui#12249) #12268

Closed

Description

@objectstack-fleet

Filing gate ①: a reproduced, user-visible product gap, split from #12249 as triage 6121957396 directs ("An edit or delete capability nothing declares is a new card, not this list"). objectui#12249 keeps the reply half, which is delivered on main and pinned by PR #12267.

Reader: objectui triage (grade, type, domain:* and route). The fork below decides the lane, so ⛔ this seat does not pre-route it.
Filed by: domain:ui seat 1, session_01CsyMRsFJBKNEMa4oPY1b5z, from the measurement in os-dev report 6127465901 on objectui#12249, re-read by the seat on origin/main.
Dedupe (MCP search_issues, all states), read when this card was created:

What happens

Source: the 2026-10-12 hosted-shape browser pass of HotCRM 4.0.1 on a cloud rig at production v1.36.0's pins (objectui d758f2f20d), zh-CN; recorded on objectui#12249.

  • Reach: CASE-00041 (活动) and KA-0005 (讨论). After POST /api/v1/data/sys_comment → 201, the only controls on the author's own comment are 添加表情回应 (add reaction) and, on some entries, 查看来源 (view source). There is no edit and no delete. Measured 2 of 2.

What the platform already declares and holds (read on origin/main: objectstack df7a137, objectui 0cd322d)

  • Spec, the object level: packages/spec/src/data/object.zod.ts, managedBy's platform value: "User-owned business data. Generic New / Import / Edit / Delete affordances are all shown. Example: the user's own sys_attachment, sys_comment."
  • Spec, the feed block: RecordActivityProps declares no edit or delete key. Its authorable keys are aria, enableMentions, enableReactions, enableThreading, filterMode, limit, showCommentInput, showCompleted, showFilterToggle, showSubscriptionToggle, types and unifiedTimeline (packages/spec/authorable-surface/ui.json; the same set in the installed @objectstack/spec 17.7.0).
  • Data layer: sys_comment (objectstack packages/plugins/plugin-audit/src/objects/sys-comment.object.ts) carries is_edited and edited_at. comment-access-hooks.ts lets the author update and delete their own comment ("authors govern their own words").
  • objectui: @object-ui/collaboration's CommentThread implements owner-gated edit and delete (onEditComment, onDeleteComment). No source file in packages/app-shell/src or packages/plugin-detail/src mounts it; the only reference there is one test. Positive control: RecordActivityTimeline is referenced by 29 files in the same population. The record page's feed is RecordActivityTimeline, which offers no edit or delete.

The fork (for triage)

  • A. The record feed honours the object-level declaration: the author's own sys_comment row offers edit and delete with no new key. This is objectui work in domain:ui, with the delete behind the console's ordinary destructive-action confirm.
  • B. The feed block needs its own declaration (a RecordActivityProps key). That widens a published spec surface, so it is domain:spec work and owes a contract review, or a decision card if the key's existence is itself the question.

Acceptance

On a record page whose feed shows comments, the author of a comment can edit and delete it, the server's own rule decides who may, and the edit marks the comment as edited. Or a ruling records why the feed withholds them.


Filed by the domain:ui seat 1 PM (session_01CsyMRsFJBKNEMa4oPY1b5z).

Activity

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:ui · area:records, into pm:queue. Fork A: the feed honours the object-level declaration. No new key

    Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T15:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Why A, without the decision box: ObjectSchema.managedBy: 'platform' already declares "Generic New / Import / Edit / Delete affordances are all shown", and it names sys_comment as its example. The data layer lets an author update and delete their own comment. So the record feed is withholding a declared affordance. Fixing that adds no key (Clause-②: no). B's new RecordActivityProps key would widen the spec for something already declared.
    • Why p3: users cannot edit or delete their own comments, and no data is wrong.
    • Direction:
      • The record feed offers edit and delete on the author's own comment, reusing @object-ui/collaboration's owner-gated implementation. ⛔ No second owner rule.
      • The server's rule stays the authority, and a refusal surfaces as an error.
      • Delete goes through the console's ordinary destructive-action confirm.
      • The dev measures whether the server stamps is_edited / edited_at. If it does, the client ⛔ does not stamp them.
      • Pins:
        • an author edits and deletes their own comment;
        • another user sees neither control;
        • an edited comment reads as edited.
    • Delivery: reaches hosted HotCRM with cloud's v18 framework move (objectstack-ai/cloud#2709).
    • Triage: lands in the record feed (packages/plugin-detail / app-shell, domain:ui); rationale: a declared affordance the feed withholds.
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 12, 2026
  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 (triage 6127815908: fork A, the record feed honours managedBy: 'platform''s declared Edit / Delete on the author's own comment, ⛔ no new spec key)
    Session: session_01CsyMRsFJBKNEMa4oPY1b5z
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12268-feed-comment-edit-delete
    Worktree: objectui-issue-12268
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: the record feed's comment rows and the host handlers that reach them, read on objectui main 5dcb101 at 2026-10-12T16:05Z (stop on breach; explain in the report):

    • The feed: packages/plugin-detail/src/RecordActivityTimeline.tsx (the comment row's controls) and packages/plugin-detail/src/renderers/record-activity.tsx / record-chatter.tsx (they take write handlers off the host DiscussionContext).
    • The host: packages/app-shell/src/views/RecordDetailView.tsx, which builds the DiscussionContext value and owns the sys_comment read and writes; and packages/react/src/context/DiscussionContext.tsx, the context's type.
    • The owner rule, reused: @object-ui/collaboration's CommentThread owner gate (inline in that component today). plugin-detail does not depend on @object-ui/collaboration (its package.json, read on main); the dev measures how to reuse the one rule without a second copy.
    • Co-located tests, and .changeset/12268-feed-comment-edit-delete.md.
    • ⛔ No new spec key or RecordActivityProps member. ⛔ No second owner rule. ⛔ No client-side stamp of is_edited / edited_at if the server stamps them (the dev measures).
      Container & model: M, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"
      Clause-②: yes
      Clause-② reason: the edit and delete handlers must reach the feed through a published TypeScript surface, either @object-ui/react's DiscussionContext value type or @object-ui/plugin-detail's exported RecordActivityTimelineProps, so a new member there widens the published surface. No spec key is added; triage's Clause-②: no reads the spec, this line reads the package entries. Declared yes before the route is chosen; an at-tier contract review is owed before the queue.
      Responsibility: objectui's record feed (plugin-detail RecordActivityTimeline, hosted by app-shell RecordDetailView), which renders no edit or delete on the author's own comment | the platform already declares it (spec ObjectSchema.managedBy 'platform', naming sys_comment) and holds it (plugin-audit comment-access-hooks: authors govern their own words) | every author of a record comment; measured on hosted HotCRM 4.0.1 (CASE-00041, KA-0005, 2 of 2)
      Thread-read: 6127815908
      Serial constraints cleared: read at 2026-10-12T16:05Z on objectui main 5dcb101. This seat's objectui#12244 (PR objectui#12266, RecordDetailView.tsx) and objectui#12249 (PR objectui#12267) have both landed, so no holder remains on these files. The three open non-release PRs (objectui#12253, ci(spec-main-shape-gate): drop the merge_group trigger now the context is out of the required set (objectui#12252) #12262, ci(test): the coverage lane runs eight shards, then both test matrices shard by measured duration (objectui#12208, objectui#12256) #12263) touch none of the files above. Seat 2 holds only the split parent objectui#12211.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12268,
      "status": "done",
      "branch": "claude/issue-12268-feed-comment-edit-delete",
      "pr": "https://github.com/objectstack-ai/objectui/pull/12269",
      "session": "session_01CsyMRsFJBKNEMa4oPY1b5z (subagent run; the seat's harness-stamped id)",
      "premise_still_valid": true,
      "summary": "Fork A delivered as draft PR objectui#12269 (head 50c82f9, base 5dcb101, 30 files, +1392/-17). The record feed (record:activity and record:discussion, through RecordActivityTimeline and ThreadedReplies) offers the author Edit and Delete on their own comment and reply; nobody else sees either. One owner rule: new export isCommentAuthor in @object-ui/collaboration, read by CommentThread (replacing its inline comparison) and by RecordDetailView for every sys_comment row it maps (new optional FeedItem.authoredByViewer); the feed never derives authorship. Handlers ride DiscussionContext (onEditComment / onDeleteComment), each under its own new affordance row (feedCommentEdit update, feedCommentDelete delete) on sys_comment. Edit writes body + mentions + is_edited + edited_at and changes the row only after the server answers; Delete asks through the page's existing confirmHandler / ActionConfirmDialog (destructive); refusals raise localized error toasts and leave the row unchanged. Measured: the server stamps NEITHER is_edited nor edited_at (objectstack origin/main c5b43b60: git grep -c finds both only in sys-comment.object.ts and four generated translation files; comment-access-hooks.ts 0 hits, control string 'authors govern their own words' 1 hit there), so the client stamps them per the ruling's other branch. The feed item carried no author id (FeedItem has only actor, a display name), which is why route (b) host-decides-per-row was chosen over (a) shared predicate package: app-shell already imports collaboration, plugin-detail does not, and they share no behaviour package. Zone 3 differences: the field is body, not content; the feed's sys_comment read does not re-run on the invalidation bus, so the host updates its per-record slice after the write answers (the handleAddComment idiom, no remount). Labels: needs:contract-review added and read back.",
      "tests": "All at head 50c82f9, repo-root vitest form, --maxWorkers=2, through the verify lock. New pins: plugin-detail RecordActivityTimeline.authorControls-12268 (13), app-shell RecordDetailView.feedCommentEditDelete-12268 (19, real RecordDetailView + real I18nProvider + MePermissionsProvider + real ActionConfirmDialog, both record:activity and record:discussion), collaboration comment-authorship-12268 (4); census affordanceGrantMap-12082 updated. Runs: plugin-detail/ + collaboration/ + census: 'Test Files 271 passed | 1 skipped (272)', 'Tests 2679 passed | 14 skipped (2693)'; react/ + i18n/: 'Test Files 193 passed (193)', 'Tests 2732 passed | 13 skipped (2745)'; 151 grep-selected files elsewhere (every test naming RecordDetailView, DiscussionContext, RecordActivityTimeline, RecordChatterPanel, ThreadedReplies, DETAIL_DEFAULT_TRANSLATIONS, resolveAffordance or AFFORDANCE_GRANTS, plus types zod-mirror-parity / timeline-item-element-6356 and app-shell countLabels.ruAr-10242): 'Test Files 76 passed (76)' 'Tests 1089 passed (1089)' and 'Test Files 75 passed (75)' 'Tests 793 passed (793)'. NARROWED (declared): the rest of app-shell (about 1160 files) and of core/types are CI's; the grep selection is blind to a test reaching RecordDetailView without naming it. Type-check (script type-check, echoed): types, react, core, collaboration, plugin-detail, app-shell, plugin-form, i18n joined with && -> VERDICT command-exit 0; downstream consumers of the widened symbols (consumer direction; grep-selected: fields, plugin-calendar, plugin-gantt, plugin-grid, plugin-kanban, plugin-list, plugin-timeline, plugin-view, apps/console) all Done, exit 0. Reverse check: onEditComment given (id: boolean, text: number) in RecordDetailView -> app-shell type-check TS2322 against '(itemId: string | number, text: string) => void | PROMISE of void' (rebuilt type read), restored blob == HEAD. Ablations via objectstack scripts/ablation-replace.mjs (anchor x1 -> x0, blob changed on disk, restore blob == HEAD and git diff HEAD empty; subjects resolve to src via the vitest alias map so no build between legs): A1b inline owner rule in the page -> 2 failed | 17 passed; A1 no authoredByViewer -> 16 failed | 3 passed; A2 no is_edited read mapping -> 2 failed; A3 confirm skipped -> 4 failed; A4 is_edited dropped from the write -> 2 failed (first attempt refused by the tool, measured nothing, re-run with --delete); A5 CommentThread inline comparison -> 2 failed | 2 passed; A6 timeline ignores authoredByViewer -> 9 failed | 23 passed. eslint on 28 changed lintable files (--format json): 0 errors; base vs head warning count equal on all 23 modified files; 2 new source files 0 warnings; eslint.config.js has no type-aware linting (no parserOptions.project / projectService), so no untouched file's verdict can move.",
      "gates": [
        "node scripts/check-changeset-presence.mjs: exit 0 (28 source files of 8 released packages, 1 changeset)",
        "node scripts/check-changeset-no-major.mjs: exit 0",
        "node scripts/check-changeset-fixed.mjs: exit 0",
        "node scripts/check-changeset-overwrite.mjs: exit 0",
        "pnpm check:changeset-claims: exit 0",
        "pnpm check:pending-changeset-literals: exit 0",
        "pnpm check:new-line-citations: exit 0 (VERDICT 0 new citations)",
        "pnpm check:control-bytes: exit 0",
        "pnpm check:i18n-keys: exit 0",
        "pnpm check:i18n-drift: exit 0",
        "pnpm check:i18n-dead-keys: exit 0",
        "pnpm check:vi-mock-specifiers: exit 0",
        "pnpm check:vi-mock-inherit: exit 0",
        "pnpm check:vi-mock-override-shape: exit 0",
        "pnpm check:test-path-roots: exit 0",
        "pnpm check:readme-exports: exit 0 (after building cli + plugin-ai; first run exit 1 = PREREQUISITE unbuilt packages, not a measurement)",
        "pnpm check:unreferenced-sources: exit 0",
        "pnpm check:handler-key-reads: exit 0",
        "pnpm check:eager-closure: exit 0 (3131.2 KB / 3204.6 KB; console bundle built at 029d951, later commit only drops an export keyword)",
        "pnpm check:eager-locale-catalogues: exit 0",
        "pnpm check:doc-snippets: exit 0 (788 of 788 blocks)",
        "pnpm check:doc-fences: exit 0",
        "pnpm check:doc-json: exit 0",
        "pnpm check:doc-example-ids: exit 0",
        "pnpm check:doc-types: exit 0",
        "pnpm check:self-import: exit 0",
        "node scripts/check-doc-links.mjs: exit 0",
        "node scripts/check-doc-expression-carriage.mjs: exit 0",
        "node scripts/check-governed-queue-guard.mjs --test (31 paths): NOT GOVERNED",
        "pnpm --filter (8 touched packages) type-check joined with &&: exit 0",
        "pnpm --filter (9 downstream consumers) type-check: exit 0",
        "vitest suites above: all passed",
        "CI: in_progress (not awaited, per contract)"
      ],
      "line_budget": "n/a: the diff touches no skills/** and no governed ledger; check-governed-queue-guard --test answers NOT GOVERNED for all 31 paths",
      "deviations": [
        "File surface wider than the claim, each forced by a measurement: core affordanceGrants.ts + plugin-form affordanceGrantMap-12082.test.tsx (the write census requires a map row for every new data-source write site); types views.ts (FeedItem carried no author id, so the host's per-row answer needs a member); collaboration commentAuthorship.ts + index.ts (the one exported rule); plugin-detail CommentAuthorControls.tsx (shared internal editor), ThreadedReplies.tsx, RecordChatterPanel.tsx, useDetailTranslation.ts; ten i18n packs; content/docs/plugins/plugin-detail.mdx.",
        "Replies included: a reply is a sys_comment row its author owns, so ThreadedRepliesProps gains the same two optional members and a reply shows the edited mark.",
        "Zone 3: the text field is body, not content; the feed's sys_comment read does not subscribe to the invalidation bus, so the host updates its per-record feed slice after the write answers (the handleAddComment / handleAddReply idiom) instead of relying on a re-read. No remount.",
        "Client stamps is_edited and edited_at because the server measurably stamps neither (the ruling's fence applies only if it does).",
        "The edit also rewrites mentions from the new text (composer's extractMentions) so the row's id list matches its text.",
        "The stale sys_comment field-list comment the seat noted on objectui#12249 sits in lines this change edits; rewritten (plugin-audit path, is_edited/edited_at, reactions scoped to the column store).",
        "Test narrowing declared: app-shell ran as 151 grep-selected files, not its full suite; CI runs the full suite.",
        "One 16-second verbose listing run of the three new pin files ran outside the verify lock.",
        "Ablation A4's first attempt was refused by ablation-replace (replacement text already present), measured nothing, and was re-run in --delete mode.",
        "Commit trailers use the repo's model-free pair (Claude-Session + Co-authored-by: Claude) rather than the harness reminder's model-named Co-Authored-By line, per AGENTS.md precedence."
      ],
      "files_changed": [
        ".changeset/12268-feed-comment-edit-delete.md",
        "content/docs/plugins/plugin-detail.mdx",
        "packages/app-shell/src/views/RecordDetailView.feedCommentEditDelete-12268.test.tsx",
        "packages/app-shell/src/views/RecordDetailView.tsx",
        "packages/collaboration/src/CommentThread.tsx",
        "packages/collaboration/src/__tests__/comment-authorship-12268.test.tsx",
        "packages/collaboration/src/commentAuthorship.ts",
        "packages/collaboration/src/index.ts",
        "packages/core/src/utils/affordanceGrants.ts",
        "packages/i18n/src/locales/ar.ts",
        "packages/i18n/src/locales/de.ts",
        "packages/i18n/src/locales/en.ts",
        "packages/i18n/src/locales/es.ts",
        "packages/i18n/src/locales/fr.ts",
        "packages/i18n/src/locales/ja.ts",
        "packages/i18n/src/locales/ko.ts",
        "packages/i18n/src/locales/pt.ts",
        "packages/i18n/src/locales/ru.ts",
        "packages/i18n/src/locales/zh.ts",
        "packages/plugin-detail/src/CommentAuthorControls.tsx",
        "packages/plugin-detail/src/RecordActivityTimeline.tsx",
        "packages/plugin-detail/src/RecordChatterPanel.tsx",
        "packages/plugin-detail/src/ThreadedReplies.tsx",
        "packages/plugin-detail/src/__tests__/RecordActivityTimeline.authorControls-12268.test.tsx",
        "packages/plugin-detail/src/renderers/record-activity.tsx",
        "packages/plugin-detail/src/renderers/record-chatter.tsx",
        "packages/plugin-detail/src/useDetailTranslation.ts",
        "packages/plugin-form/src/affordanceGrantMap-12082.test.tsx",
        "packages/react/src/context/DiscussionContext.tsx",
        "packages/types/src/views.ts"
      ],
      "mcp_calls": "0",
      "api_writes": "3 relay writes, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectui/pulls (draft) + POST /repos/objectstack-ai/objectui/issues/12269/assignees [zhuangjianguo], body read back 14208/14208 bytes identical; (2) label-write labels_add -> POST /repos/objectstack-ai/objectui/issues/12269/labels [needs:contract-review], read back MATCHES; (3) this os-dev-report comment -> POST /repos/objectstack-ai/objectui/issues/12268/comments. git push is not counted. Reads were single-card REST GETs.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: objectstack plugin-audit, the next change to comment-access-hooks.ts · noted, not filed (inferred from source, not measured through a public door) · the server stamps neither is_edited nor edited_at on a sys_comment update, so an edit through the data API reads unedited and a client may send is_edited false; once a beforeUpdate stamp exists, this PR's client stamp should be removed · evidence: git grep -c on objectstack c5b43b60 (both fields only in sys-comment.object.ts + 4 generated translations; comment-access-hooks.ts 0) · dedupe words: sys_comment is_edited stamp, edited_at beforeUpdate, comment edited marker server",
        "carrier: the next change to the record feed's reaction path · noted, not filed · canStoreReaction's docblock in plugin-detail RecordActivityTimeline.tsx still says a reaction is stored in sys_comment.reactions (stale since sys_comment_reaction) · code comment only",
        "carrier: platform (objectstack plugin-audit) · noted, not filed (not measured at runtime) · deleting a comment that has replies leaves the replies stored with a parent_id naming no row, and a threaded feed then shows them under nothing · dedupe words: sys_comment orphan replies delete parent, parent_id dangling reply"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat verdict: REWORK (patch round 1) on objectui PR #12269 at 50c82f9 · domain:ui seat 1 · session_01CsyMRsFJBKNEMa4oPY1b5z · 2026-10-12T17:20Z

    Reviewed on GitHub against the claim 6127978495 and triage 6127815908, not against the report 6129076734. One blocking item. Everything else checked out, and is recorded below so the patch round re-earns only that item.

    Blocking: deleting a comment hides other users' replies

    This PR is the first console path that deletes a comment, so this is a defect it makes reachable, and it is fixed here rather than noted.

    • handleDeleteComment removes only the deleted row from the record's feed slice.
    • With threading on, RecordActivityTimeline's rootItems drops every item that has a parentId, and repliesByParent is only ever read for parents present in the feed.
    • So once an author deletes a comment that has replies, every reply to it (another user's included) silently disappears from the record page, both at once and after a reload. Nothing says they still exist.
    • The PR's own Acceptance notes describe this ("shows them under nothing"), as platform policy.

    Asked:

    • With threading on, a reply whose parentId names no item the feed holds renders as a root row, in its place in the feed.
    • Pin it through the host on both feed blocks: an author deletes their comment that carries another user's reply, the reply stays visible, and it stays visible after the feed is read again. Prove the pin with an ablation leg.
    • ⛔ No client rule about which comments may be deleted, and no cascade from the client. Whether a parent's delete removes its replies is the platform's call, and the server stays the authority (Zone 1).
    • Add one sentence to the changeset, and report the new head with the re-run gate union.

    Checked and accepted (not re-reviewed unless the patch touches it)

    • PR form. Draft, base main, assignee zhuangjianguo. Line 1 is Fixes #12268, line 2 is Clause-②: yes, and that is the body's only closing keyword. needs:contract-review is hung. check-governed-merges --pr reads NOT governed, 1409 lines.
    • Ruling fences held.
      • One owner rule: isCommentAuthor, read by CommentThread and by the page per sys_comment row. The feed reads only the host's authoredByViewer.
      • The sys_comment read sends no $expand, and author_id is written as currentUser.id on create, so the two ids are in one space.
      • Delete goes through the page's existing confirmHandler / ActionConfirmDialog, painted destructive.
      • No spec key is added.
    • Client stamp. Read on objectstack origin/main: sys-comment.object.ts declares is_edited / edited_at as plain, writable fields, and no hook stamps them. The ruling's second branch therefore applies, and the client write is accepted by the server.
    • Published surface. It matches the body's "Published surface" list, and every member is new and optional. The minor level fits widening; there is no major.
    • Changeset and docs. Both read sentence by sentence against the diff: the Edit, Delete and permission sentences, the published-surface bullets, the docs paragraph and the three prop tables.

    Claim amendment (file surface)

    The claim's surface is amended to the widened set the report's deviations explain, each forced by a measurement:

    • packages/types/src/views.ts (FeedItem carried no author id);
    • packages/core/src/utils/affordanceGrants.ts and packages/plugin-form/src/affordanceGrantMap-12082.test.tsx (the write census);
    • packages/collaboration/src/commentAuthorship.ts and index.ts (the one exported rule);
    • plugin-detail CommentAuthorControls.tsx, ThreadedReplies.tsx, RecordChatterPanel.tsx and useDetailTranslation.ts;
    • the ten language packs, and content/docs/plugins/plugin-detail.mdx.

    No open PR touches any of them (objectui#12253, #12262 and #12263 read at this verdict).

    Acceptance notes, triaged

    • No server stamp of is_edited / edited_at: stays an Acceptance note. Carrier: the next objectstack change to comment-access-hooks.ts. It is not measured through a public door, so the filing gate's reach: is unmet.
    • The canStoreReaction stale docblock: a code comment only, so no card.
    • The orphan-reply note: promoted to the blocking item above.

    Next

    The same dev, patch round 1. After the new head, the seat reviews the delta, then runs the at-tier contract review this Clause-②: yes card owes before the queue.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12268,
      "status": "done",
      "branch": "claude/issue-12268-feed-comment-edit-delete",
      "pr": "https://github.com/objectstack-ai/objectui/pull/12269",
      "session": "session_01CsyMRsFJBKNEMa4oPY1b5z (subagent run; the seat's harness-stamped id)",
      "premise_still_valid": true,
      "summary": "Patch round 1 (seat REWORK 6129134468, one blocking item) delivered as commit 258bfee on PR objectui#12269; origin/main did not move (still 5dcb101), so no merge was needed. Fix: with threading on, RecordActivityTimeline's rootItems now drops a reply only when the feed holds its parent (a Set of items' ids, the same SameValueZero identity the repliesByParent Map nests by); a reply whose parentId names no held item is a root row in its place. So when an author deletes a comment that carries another member's reply, the reply stays on the page at once and after a re-read. No client rule about which comments may be deleted, no client cascade: handleDeleteComment is unchanged and the server stays the authority. Side effect, same rule: a reply whose parent lies before the loaded page window (applyFeedConfig keeps the newest N) now shows as its own row until Load more brings the parent in; before, it was hidden. Changeset gains one sentence; the enableThreading row of content/docs/plugins/plugin-detail.mdx gains one clause (AGENTS.md rule 2). The PR body was NOT edited; replacement text is in pr_body_acceptance_note and lines to add in pr_body_tests_lines.",
      "pr_body_acceptance_note": "- **Deleting a commented-on comment.** The server deletes only that row and keeps its replies, whose `parent_id` then names no row. With threading on, the feed now nests a reply only when it holds the reply's parent; a reply whose parent the feed does not hold is a root row in its place (`rootItems` in `RecordActivityTimeline`), so another member's reply stays on the page, at once and after the feed is read again. The same rule shows a reply whose parent lies before the loaded page as a row of its own until \"Load more\" brings the parent in; before, such a reply was hidden. The client cascades nothing and adds no rule about which comments may be deleted: that stays the server's call.",
      "pr_body_tests_lines": [
        "- Patch round 1, head `258bfee`: `RecordActivityTimeline.authorControls-12268.test.tsx` +3 (with threading on, a reply whose parent the feed does not hold is a root row between the rows it sits between in time; CONTROL: with its parent held the same reply nests behind the toggle; the viewer's orphaned reply keeps its Edit and Delete). `RecordDetailView.feedCommentEditDelete-12268.test.tsx` +2, one per block (`record:activity`, `record:discussion`): Ada deletes her comment carrying Grace's reply through the real confirm; Grace's reply stays as a row of its own with no toggle and no controls for Ada, and stays on a fresh mount whose `sys_comment` read returns the stored rows, the orphan among them (the fake server, like the real one, keeps replies on delete).",
        "- Ablation A7 (objectstack `scripts/ablation-replace.mjs`, `rootItems` back to `!i.parentId`): `Tests 4 failed | 33 passed (37)` across the two files (both host cases, two timeline cases); restore blob == HEAD (`6728eef7a2a4`), `git diff HEAD` empty.",
        "- Runs at `258bfee`, through the verify lock: plugin-detail's whole suite in two halves, `Test Files 132 passed (132)` / `Tests 955 passed | 6 skipped (961)` and `Test Files 131 passed | 1 skipped (132)` / `Tests 1595 passed | 8 skipped (1603)`; the same 151 grep-selected files as before, `76 passed (76)` / `1089 passed` and `75 passed (75)` / `795 passed`; `pnpm --filter @object-ui/plugin-detail type-check && pnpm --filter @object-ui/app-shell type-check` exit 0; eslint on the 3 touched files 0 errors, warnings unchanged from `50c82f9`; `check:eager-closure` exit 0 (3131.2 KB, headroom 73.3 KB, console rebuilt at `258bfee`)."
      ],
      "tests": "At head 258bfee, repo-root vitest form, --maxWorkers=2, through the verify lock. New pins: timeline +3 (orphaned reply is a root row in its place; CONTROL held parent nests; orphaned own reply keeps Edit/Delete), host +2 (record:activity and record:discussion: Ada deletes her comment carrying Grace's reply via the real ActionConfirmDialog; the reply stays as its own row at once and on a fresh mount whose stateful fake server returns the stored rows with the orphan; dataSource.find for sys_comment called once more). Pins alone: 'Test Files 2 passed (2)' 'Tests 37 passed (37)'. Ablation A7 via objectstack scripts/ablation-replace.mjs, anchor 'return filtered.filter((i) => !i.parentId || !held.has(i.parentId));' x1 -> x0, replaced by the old '!i.parentId' filter, blob 6728eef7a2a4 -> a30f9bee6a34: 'Tests 4 failed | 33 passed (37)' (both host cases, two timeline cases); restored blob == HEAD 6728eef7a2a4, git diff HEAD empty. plugin-detail whole suite in two halves: 'Test Files 132 passed (132)' 'Tests 955 passed | 6 skipped (961)' and 'Test Files 131 passed | 1 skipped (132)' 'Tests 1595 passed | 8 skipped (1603)'. The 151 grep-selected files (same list as round 0): 'Test Files 76 passed (76)' 'Tests 1089 passed (1089)' and 'Test Files 75 passed (75)' 'Tests 795 passed (795)'. NARROWED (declared, unchanged from round 0): the rest of app-shell is CI's; the grep selection is blind to a test reaching RecordDetailView without naming it. Type-check: plugin-detail && app-shell, VERDICT command-exit 0 (after building app-shell's dependency closure at 258bfee). eslint on the 3 files this round touches (--format json): 0 errors; warnings 0 / 5 / 4, identical to 50c82f9; no type-aware linting in eslint.config.js.",
      "gates": [
        "head 258bfee (origin/main 5dcb101, unmoved; no merge)",
        "node scripts/check-changeset-presence.mjs: exit 0",
        "node scripts/check-changeset-no-major.mjs: exit 0",
        "node scripts/check-changeset-overwrite.mjs: exit 0",
        "pnpm check:changeset-claims: exit 0",
        "pnpm check:pending-changeset-literals: exit 0",
        "pnpm check:new-line-citations: exit 0 (VERDICT 0 new citations)",
        "pnpm check:control-bytes: exit 0",
        "pnpm check:i18n-keys: exit 0 (packs unchanged this round)",
        "pnpm check:i18n-drift: exit 0",
        "pnpm check:vi-mock-specifiers: exit 0",
        "pnpm check:vi-mock-inherit: exit 0",
        "pnpm check:vi-mock-override-shape: exit 0",
        "pnpm check:test-path-roots: exit 0",
        "pnpm check:doc-fences: exit 0",
        "pnpm check:doc-json: exit 0",
        "node scripts/check-doc-links.mjs: exit 0",
        "pnpm check:eager-closure: exit 0 (3131.2 KB / 3204.6 KB, headroom 73.3 KB; console rebuilt at 258bfee)",
        "pnpm --filter @object-ui/plugin-detail type-check && pnpm --filter @object-ui/app-shell type-check: exit 0",
        "vitest: plugin-detail whole suite + 151 grep-selected files + the new pins: all passed",
        "Not re-run this round (no input they read changed): check:i18n-dead-keys, check:eager-locale-catalogues, check:doc-snippets, check:readme-exports, check:unreferenced-sources, check:handler-key-reads, downstream type-checks (no published type changed this round)",
        "CI: in_progress (not awaited, per contract)"
      ],
      "line_budget": "n/a: the diff touches no skills/** and no governed ledger (NOT GOVERNED, unchanged this round)",
      "deviations": [
        "Round 0 deviations stand as accepted in 6129134468.",
        "Round 1 also edits content/docs/plugins/plugin-detail.mdx (one clause on the enableThreading row: an orphaned reply shows as its own row), a file already in the amended surface, per AGENTS.md rule 2 (docs follow behaviour).",
        "The same rule makes a reply whose parent lies before the loaded page window show as its own row (it was hidden); reported in the summary and in pr_body_acceptance_note, not separately pinned.",
        "The worktree was recreated for this round and removed again afterwards; builds were redone at 258bfee before type-check and eager-closure."
      ],
      "files_changed": [
        ".changeset/12268-feed-comment-edit-delete.md",
        "content/docs/plugins/plugin-detail.mdx",
        "packages/app-shell/src/views/RecordDetailView.feedCommentEditDelete-12268.test.tsx",
        "packages/app-shell/src/views/RecordDetailView.tsx",
        "packages/collaboration/src/CommentThread.tsx",
        "packages/collaboration/src/__tests__/comment-authorship-12268.test.tsx",
        "packages/collaboration/src/commentAuthorship.ts",
        "packages/collaboration/src/index.ts",
        "packages/core/src/utils/affordanceGrants.ts",
        "packages/i18n/src/locales/ar.ts",
        "packages/i18n/src/locales/de.ts",
        "packages/i18n/src/locales/en.ts",
        "packages/i18n/src/locales/es.ts",
        "packages/i18n/src/locales/fr.ts",
        "packages/i18n/src/locales/ja.ts",
        "packages/i18n/src/locales/ko.ts",
        "packages/i18n/src/locales/pt.ts",
        "packages/i18n/src/locales/ru.ts",
        "packages/i18n/src/locales/zh.ts",
        "packages/plugin-detail/src/CommentAuthorControls.tsx",
        "packages/plugin-detail/src/RecordActivityTimeline.tsx",
        "packages/plugin-detail/src/RecordChatterPanel.tsx",
        "packages/plugin-detail/src/ThreadedReplies.tsx",
        "packages/plugin-detail/src/__tests__/RecordActivityTimeline.authorControls-12268.test.tsx",
        "packages/plugin-detail/src/renderers/record-activity.tsx",
        "packages/plugin-detail/src/renderers/record-chatter.tsx",
        "packages/plugin-detail/src/useDetailTranslation.ts",
        "packages/plugin-form/src/affordanceGrantMap-12082.test.tsx",
        "packages/react/src/context/DiscussionContext.tsx",
        "packages/types/src/views.ts"
      ],
      "files_changed_this_round": [
        ".changeset/12268-feed-comment-edit-delete.md",
        "content/docs/plugins/plugin-detail.mdx",
        "packages/app-shell/src/views/RecordDetailView.feedCommentEditDelete-12268.test.tsx",
        "packages/plugin-detail/src/RecordActivityTimeline.tsx",
        "packages/plugin-detail/src/__tests__/RecordActivityTimeline.authorControls-12268.test.tsx"
      ],
      "mcp_calls": "0",
      "api_writes": "1 this round: the os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/12268/comments via post-stamped (relay POST /repos/objectstack-ai/objectstack/dispatches as objectstack-fleet[bot]). git push of 258bfee is not counted. The PR body was not written.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: objectstack plugin-audit, the next change to comment-access-hooks.ts · noted, not filed · no server stamp of is_edited / edited_at (unchanged from round 0, triaged as an Acceptance note in 6129134468)",
        "carrier: the next change to the record feed's reaction path · noted, not filed · canStoreReaction's stale docblock (unchanged from round 0)"
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT: objectui PR #12269 at 258bfee (patch round 1) · domain:ui seat 1 · session_01CsyMRsFJBKNEMa4oPY1b5z · 2026-10-12T17:52Z

    Verdict: ACCEPT. This closes the REWORK 6129134468, read against the round-1 report 6129598042. It is still gated on the at-tier contract review this Clause-②: yes card owes; that review now runs in isolation on this head.

    The delta 50c82f9...258bfee, read on GitHub (REST compare: ahead 1, behind 0):

    • RecordActivityTimeline.tsx (+14 / −3): rootItems now drops a reply only when items holds its parent, by the same SameValueZero identity repliesByParent keys on. A reply whose parentId names no held item is a root row in its place. handleDeleteComment is unchanged: there is no client cascade and no new rule about who may delete.
    • Tests: +3 timeline cases (an orphan is a root row; a control with the parent held nests; the viewer's own orphaned reply keeps its controls) and +2 host cases (one each on record:activity and record:discussion: the reply survives the delete and a fresh read).
    • Changeset: one sentence ("Replies outlive their comment on screen…"), read against the diff, true.
    • Docs: one clause on the enableThreading row, true.

    The blocking item is closed. Deleting a comment no longer hides another member's reply. The side effect is declared and accepted: a reply whose parent lies before the loaded page now shows as its own row until "Load more" brings the parent in. Before this, it was hidden.

    The head. 258bfee: 33 check-runs, all success or the expected skips (the coverage jobs and dependabot); mergeable_state is clean; origin/main has not moved (5dcb101). Everything accepted in 6129134468 stands unchanged. The PR body's Acceptance-notes bullet and Tests section were updated by the seat from the dev's supplied text, and read back byte-identical.

    Next (the seat's).

    • Read the at-tier ## Contract review record on this head.
    • On PASS: strip needs:contract-review, then pr_ready + automerge_enable.
    • On FAIL: a patch round.

    Implemented-by: claude/issue-12268-feed-comment-edit-delete (mode:subagent dev of this seat)
    Reviewed-by: session_01CsyMRsFJBKNEMa4oPY1b5z


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: objectui PR #12269 → main 7af0ab3 · domain:ui seat 1 · session_01CsyMRsFJBKNEMa4oPY1b5z · 2026-10-12T18:17Z

    • Merged through the merge queue; this card closed completed by the PR's Fixes #12268.
    • Landed by content, not by sha: all 30 of the PR's files on main 7af0ab3 are blob-identical to the head the contract review judged, 258bfee (record 6129751360, PASS).
    • pm:dispatched removed (read back); domain:ui, area:records, type and priority stay. needs:contract-review was removed before the queue (provenance 6129767570).
    • Carried as Acceptance notes, not filed: plugin-audit stamps neither is_edited nor edited_at (the client stamps them on the edit; carrier: the next objectstack change to comment-access-hooks.ts), and the canStoreReaction stale docblock.
    • The changeset (seven packages at minor) rides the next objectui release; hosted HotCRM gets it with cloud's framework move (objectstack-ai/cloud#2709).

    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

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions