|
2686 | 2686 | "title": "Record discussion: an @mention comment reconciles optimistically, persists to sys_comment, interleaves with activity, pings the mentioned user's bell — and the feed filter shows exactly each mode's subset without dropping author-extended activity", |
2687 | 2687 | "since": "v16", |
2688 | 2688 | "status": "active", |
2689 | | - "revision": 4, |
| 2689 | + "revision": 5, |
2690 | 2690 | "priority": "P1", |
2691 | 2691 | "surface": "browser", |
2692 | 2692 | "personas": [ |
|
2697 | 2697 | "app": "showcase", |
2698 | 2698 | "requires": [ |
2699 | 2699 | "a showcase object with feeds enabled (default — enable.feeds !== false) so the discussion panel mounts and sys_comment writes are accepted (packages/spec/src/data/object.zod.ts feeds default true; plugin-audit enforceFeedsCapability)", |
2700 | | - "a record to comment on (e.g. a seeded showcase_task or showcase_account)" |
| 2700 | + "a record to comment on (e.g. a seeded showcase_task or showcase_account)", |
| 2701 | + "for the recipient read-gate clause (acceptance[9]) on a stock `single` boot: the knownGaps[0] second user, a member of the same organization, and two admin-owned records — a showcase_private_note (sharingModel 'private', owner-only, examples/app-showcase/src/data/objects/private-note.object.ts), which the member cannot read, and a showcase_account (public_read_write), which the member can. The cross-organization leg needs a walled boot (RUNNER.md 'A walled boot is a fixture of its own') holding two organizations, with the comment's record in one and the mentioned user a member of the other only" |
2701 | 2702 | ], |
2702 | 2703 | "provisioning": { |
2703 | 2704 | "use": "qa-feeds-disabled", |
2704 | 2705 | "why": "unblocks acceptance clauses 6 and 7 — the create-side and re-thread-side FEEDS_DISABLED probes. Without the recipe there is no feeds-disabled parent anywhere in showcase (feeds is opt-out, default true), so both clauses score blocked(fixture) and FEEDS_DISABLED survives in this item's `negative` list alone: named, never driven. That is the exact gap #12118 records, one area over from the files-side twin #12116 repaired." |
2705 | 2706 | }, |
2706 | 2707 | "knownGaps": [ |
2707 | 2708 | "the @mention TARGET and the bell/notification clauses need a SECOND signed-up user — a fresh single-user boot cannot exercise them; record those clauses blocked(fixture) and run the optimistic-reconcile + sys_comment + activity-interleave clauses as the single author", |
2708 | | - "CLOSED by the qa-feeds-disabled recipe (#12118) for the two FEEDS_DISABLED clauses: before it, no showcase object declared enable.feeds:false and there was no scratch recipe for this area, so the code appeared only in `negative`. Fall back to the pinned engine/API suites named in `automated` ONLY if the recipe cannot be replayed on the boot under test — and when you do, record WHICH of the two the verdict rests on, because the pins run against their own fixtures (cmt_nofeeds / lead_walled) and not against the showcase surface this item otherwise drives." |
| 2709 | + "CLOSED by the qa-feeds-disabled recipe (#12118) for the two FEEDS_DISABLED clauses: before it, no showcase object declared enable.feeds:false and there was no scratch recipe for this area, so the code appeared only in `negative`. Fall back to the pinned engine/API suites named in `automated` ONLY if the recipe cannot be replayed on the boot under test — and when you do, record WHICH of the two the verdict rests on, because the pins run against their own fixtures (cmt_nofeeds / lead_walled) and not against the showcase surface this item otherwise drives.", |
| 2710 | + "acceptance[9]'s cross-organization leg runs live only on a walled boot (isolated or group), which the stock showcase does not provision. A run without one records that leg blocked(fixture) citing RUNNER.md's walled entry. Its pin (automated.ref, packages/plugins/plugin-audit/src/mention-recipient-gate.integration.test.ts) reaches the ENGINE layer, not HTTP: a real kernel, ObjectQL engine and SQLite driver with AuditPlugin and the real core grant resolver, while plugin-security's read layer is played by one middleware composing the posture's tenant clause and an owner-only rule, and messaging is played by an emit() recorder. Under RUNNER rule 6 the pin scores the clause partial at most, naming the HTTP layer and the real plugin-security as missing; the stock `single` leg is driven live" |
2709 | 2711 | ] |
2710 | 2712 | }, |
2711 | 2713 | "steps": [ |
|
2721 | 2723 | "RE-THREAD (UPDATE-SIDE) FEEDS_DISABLED probe — take the sys_comment row posted earlier against the feeds-ENABLED record and PATCH /api/v1/data/sys_comment/<its id> with { thread_id: 'qa_nofeeds:<the throwaway record id>' } as admin, capturing the refusal. This is the UPDATE half of the #10170 gate (enforceFeedsCapability registered on beforeUpdate as well as beforeInsert, packages/plugins/plugin-audit/src/audit-writers.ts): re-read the comment afterward and confirm its thread_id is UNCHANGED", |
2722 | 2724 | "control for both probes — repeat the same two calls against a feeds-ENABLED parent (the seeded showcase record used above) and confirm they SUCCEED. A 403 that also fires on the enabled parent is measuring access, not the capability gate, and neither clause may be scored from it", |
2723 | 2725 | "FEED FILTER — on a record whose feed now holds at least one comment (posted above) and one field-change activity (the attributable edit above), cycle the filter control through all four FeedFilterMode values, screenshotting each state before reading the rendered rows", |
2724 | | - "OPEN-VOCABULARY probe — plant an author-EXTENDED activity type: sys_activity narrows apiMethods to ['get','list'] (sys-activity.object.ts), so a direct POST is refused BY DESIGN — the sanctioned channel is a scratch object declaring activityMilestones[].type with a custom value (ADR-0052 §5b.2, forwarded verbatim) whose milestone the run then triggers; if that authoring is unavailable on the boot under test, score the clause from the objectui pin (recordActivityFeed.test.ts) and record the substitution" |
| 2726 | + "OPEN-VOCABULARY probe — plant an author-EXTENDED activity type: sys_activity narrows apiMethods to ['get','list'] (sys-activity.object.ts), so a direct POST is refused BY DESIGN — the sanctioned channel is a scratch object declaring activityMilestones[].type with a custom value (ADR-0052 §5b.2, forwarded verbatim) whose milestone the run then triggers; if that authoring is unavailable on the boot under test, score the clause from the objectui pin (recordActivityFeed.test.ts) and record the substitution", |
| 2727 | + "RECIPIENT READ GATE (acceptance[9]) — on a stock `single` boot with fixtures.requires[2]. The stock boot's audience posture is invite_only, so POST /api/v1/auth/sign-up/email answers 403 SELF_REGISTRATION_CLOSED: the admin mints the member with POST /api/v1/auth/admin/create-user (it lands in the default organization with mustChangePassword true), and the member signs in, calls POST /api/v1/auth/change-password and signs in again. First prove the member's access as the member (GET /api/v1/data/showcase_private_note/ID answers 404, GET /api/v1/data/showcase_account/ID answers 200) and read the member's and the admin's GET /api/v1/notifications. As admin, POST /api/v1/data/sys_comment on the note's thread (thread_id 'showcase_private_note:ID') with mentions naming the member by user id, then a second comment there naming the member by email, then one on the account's thread naming the member by user id and the admin itself. Re-read both inboxes and count the rows of type collab.mention per comment (each row's body is the comment's preview). On a walled boot, repeat the note-shaped leg with the record in organization A and the mentioned user a member of organization B only" |
2725 | 2728 | ], |
2726 | 2729 | "acceptance": [ |
2727 | 2730 | { |
|
2777 | 2780 | "oracle": "dom", |
2778 | 2781 | "verify": "the planted custom-type row appears in the 'all' feed with the generic presentation (objectui recordActivityFeed.ts: ACTIVITY_TYPE_TO_FEED_TYPE is a SUPERSET pin over built-ins, unmapped values route to UNMAPPED_ACTIVITY_FEED_TYPE 'system' — objectui#5840/#5969 history); where the milestone channel is unavailable, the pin recordActivityFeed.test.ts is the fallback oracle — record which the verdict rests on", |
2779 | 2782 | "evidence": "the 'all' feed screenshot showing the custom-type row (or the pin output + the substitution note)" |
| 2783 | + }, |
| 2784 | + { |
| 2785 | + "clause": "an @mention notification reaches a mentioned user only when that user can READ the comment's record — whether the mention names them by user id or by email — and the author is never notified: the collab.mention notification carries the comment's preview and a deep link to the record, so it reaches exactly the readers the thread itself reaches. The gate sits at the producer (plugin-audit's mention writer), judged in each mentioned user's own envelope by the same grant resolver, organization-claim vetting and parent-read evaluator a request uses, so the tenant wall, sharing, RLS and object CRUD decide (#24785, fixed by #24794)", |
| 2786 | + "oracle": "api", |
| 2787 | + "verify": "step-14 inbox reads: the member gains NO collab.mention for either comment on the private note (by id, by email), gains exactly one for the account comment (data.notifications[] row of type collab.mention, actionUrl '/showcase_account/ID'), unreadCount up by one, with that deep link naming the account record and that record readable by the member (200); the admin's inbox gains none for any of the three, though the account comment named the admin. Each recipient reads only their own inbox, so the count is taken per user over GET /api/v1/notifications, never from sys_notification as admin alone. Each mentioned user withheld for want of read access is logged once at warn — '[audit] mention notification withheld' with reason cannot_read_source, the user id (the email mention resolved to it) and the comment id, never the comment text — record both lines; the author is dropped without a line. On a walled boot the cross-organization user's inbox is unchanged. Score the presence or absence of a collab.mention row per recipient and comment, not the notification's wording", |
| 2788 | + "evidence": "both users' inbox reads before and after, the member's two access reads, the three comment create responses, and the withheld-recipient log lines" |
2780 | 2789 | } |
2781 | 2790 | ], |
2782 | 2791 | "negative": [ |
|
2785 | 2794 | "feeds:false must HIDE the panel, SKIP the sys_comment fetch, AND the server must reject new comments with 403 FEEDS_DISABLED — a silent no-op that accepts a comment nowhere-readable is a FAIL. The two server halves are now DRIVEN by acceptance clauses 6 and 7 against the qa-feeds-disabled recipe's qa_nofeeds parent; until #12118 this bullet was the only place the code appeared in this area, which made it read as covered while nothing provoked it", |
2786 | 2795 | "a re-thread that is refused but LEAVES the row moved (or is accepted outright) is a FAIL — #10170 registered the gate on beforeUpdate precisely because a caller barred from creating a comment on a feeds-disabled object could otherwise re-point an existing one into it", |
2787 | 2796 | "scoring either FEEDS_DISABLED clause from the ENABLED control alone is a FAIL — the control exists to prove the refusal is attributable to the capability gate rather than to record access (RECORD_NOT_ACCESSIBLE, comment-access-hooks.ts), so a run that reports the 403 without the 2xx control has not measured the gate", |
2788 | | - "a filter mode showing a row of another mode's type — or dropping one of its own — is a FAIL; and an unknown activity type absent from the 'all' feed is a FAIL (the objectui#5840 shape: stored, queryable, invisible), while its absence from the three NARROWING modes is correct behavior a run must not file as the drop" |
| 2797 | + "a filter mode showing a row of another mode's type — or dropping one of its own — is a FAIL; and an unknown activity type absent from the 'all' feed is a FAIL (the objectui#5840 shape: stored, queryable, invisible), while its absence from the three NARROWING modes is correct behavior a run must not file as the drop", |
| 2798 | + "a collab.mention notification reaching a mentioned user who cannot read the comment's record is a FAIL in either spelling of the mention (user id or email): the notification is a copy of what the thread shows, preview and deep link included (#24785). A notification withheld from a mentioned user who CAN read the record is a FAIL the other way: the gate is read access, not organization membership or the author's own view" |
2789 | 2799 | ], |
2790 | 2800 | "variants": [ |
2791 | 2801 | "all — the full merged comment + activity feed", |
|
2811 | 2821 | "framework: packages/spec/src/data/object.zod.ts#enable.feeds (enable.feeds default true → FEEDS_DISABLED — re-pointed #18104: FEEDS_DISABLED is named in this file only inside a describe() string and a comment; the key this item means is enable.feeds, 'a new comment and an update that re-threads an existing one alike'); PENDING-GAPS §B record-discussion-mentions", |
2812 | 2822 | "framework: packages/plugins/plugin-audit/src/audit-writers.ts#enforceFeedsCapability (enforceFeedsCapability, registered on beforeInsert AND beforeUpdate per #10170), packages/plugins/plugin-audit/src/comment-access-hooks.ts#RECORD_NOT_ACCESSIBLE (RECORD_NOT_ACCESSIBLE — the sibling refusal the probes isolate against), packages/plugins/plugin-audit/src/objects/sys-comment.object.ts#thread_id (thread_id is conventionally `{object}:{record_id}`)", |
2813 | 2823 | "framework: packages/spec/src/data/feed.zod.ts#FeedFilterMode (FeedFilterMode, 4 members — pinned above),+ (SYS_ACTIVITY_BUILTIN_TYPES + the #11507 open-vocabulary ruling: built-ins are the floor, never the ceiling); packages/plugins/plugin-audit/src/objects/sys-activity.object.ts#apiMethods (apiMethods ['get','list'] — why the probe cannot direct-POST)", |
2814 | | - "objectui: packages/plugin-detail/src/RecordActivityTimeline.tsx (FeedFilterMode imported from the spec), (the four filter options), (filterItems); packages/plugin-detail/src/renderers/recordActivityFeed.ts (ACTIVITY_TYPE_TO_FEED_TYPE superset map + UNMAPPED_ACTIVITY_FEED_TYPE fallback + its diagnostic; drift held by the objectui#5969 two-directional pin, recordActivityFeed.test.ts)" |
| 2824 | + "objectui: packages/plugin-detail/src/RecordActivityTimeline.tsx (FeedFilterMode imported from the spec), (the four filter options), (filterItems); packages/plugin-detail/src/renderers/recordActivityFeed.ts (ACTIVITY_TYPE_TO_FEED_TYPE superset map + UNMAPPED_ACTIVITY_FEED_TYPE fallback + its diagnostic; drift held by the objectui#5969 two-directional pin, recordActivityFeed.test.ts)", |
| 2825 | + "framework: packages/plugins/plugin-audit/src/mention-recipient-gate.ts (the @mention recipient gate: each mentioned user judged by resolveUserAuthzGrants, vetOrganizationClaim and resolveReadableParentIds; fail-closed; #24794)", |
| 2826 | + "QA-source: #24783 · records-forms.record-discussion-mentions · acceptance[4]" |
2815 | 2827 | ], |
2816 | 2828 | "automated": { |
2817 | 2829 | "kind": "api", |
2818 | | - "ref": "The FEEDS_DISABLED clauses (6, 7) have engine/API pins that predate this item's coverage of them: packages/plugins/plugin-audit/src/capability-gate-update-verb.test.ts (describe '[#10170] enable.feeds is asked on the UPDATE verb too' — by-id and predicate re-thread onto `lead_walled`, plus the insert-verb control and the absent-thread_id boundary) and packages/qa/dogfood/test/comments-permission-matrix.dogfood.test.ts (the create side over real REST against the `cmt_nofeeds` fixture, packages/qa/dogfood/test/fixtures/comments-fixture.ts). ⚠️ Those pins run against their OWN fixtures and the objectql/REST seams — they do NOT reach the console half of the negative (panel hidden, sys_comment fetch skipped), and they never exercised the showcase surface, which is why clauses 6 and 7 still carry their own steps and the qa-feeds-disabled recipe. The remaining clauses (1-5: optimistic reconcile, activity interleave, mention bell) are browser-only and have no pin." |
| 2830 | + "ref": "The FEEDS_DISABLED clauses (6, 7) have engine/API pins that predate this item's coverage of them: packages/plugins/plugin-audit/src/capability-gate-update-verb.test.ts (describe '[#10170] enable.feeds is asked on the UPDATE verb too' — by-id and predicate re-thread onto `lead_walled`, plus the insert-verb control and the absent-thread_id boundary) and packages/qa/dogfood/test/comments-permission-matrix.dogfood.test.ts (the create side over real REST against the `cmt_nofeeds` fixture, packages/qa/dogfood/test/fixtures/comments-fixture.ts). ⚠️ Those pins run against their OWN fixtures and the objectql/REST seams — they do NOT reach the console half of the negative (panel hidden, sys_comment fetch skipped), and they never exercised the showcase surface, which is why clauses 6 and 7 still carry their own steps and the qa-feeds-disabled recipe. The remaining clauses (1-5: optimistic reconcile, activity interleave, mention bell) are browser-only and have no pin. The recipient read gate (acceptance[9]) is pinned by packages/plugins/plugin-audit/src/mention-recipient-gate.integration.test.ts (#24794: isolated, group and single cases, id and email spellings, author exclusion, the withheld-recipient log line), which reaches the ENGINE layer with plugin-security's read layer played by a middleware (knownGaps[2]); it scores that clause partial at most under RUNNER rule 6." |
2819 | 2831 | }, |
2820 | 2832 | "history": [ |
2821 | 2833 | { |
|
2841 | 2853 | "date": "2026-10-19", |
2842 | 2854 | "change": "steps[3], steps[6], acceptance[3].verify, acceptance[4] (clause, verify) and source[2] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. The bell is server-backed: plugin-audit's audit-writers.ts writes a collab.mention sys_notification per mentioned user and the console bell reads it through useInboxBell.ts (GET /api/v1/notifications); objectui's useMentionNotifications, which the item cited as the bell, has no console consumer. And no feed refetch follows a post within ~6 s, so the reconcile step reloads. Clause substance unchanged. Wave anchor #24579 round 2 §3 (6272314386)", |
2843 | 2855 | "ref": "#24671" |
| 2856 | + }, |
| 2857 | + { |
| 2858 | + "revision": 5, |
| 2859 | + "date": "2026-10-20", |
| 2860 | + "change": "acceptance[9], steps[13], negative[6], fixtures.requires[2] and knownGaps[2] added, and automated.ref names the recipient-gate pin and the layer it drives — the clause #24802 deferred under RUNNER rule 2 until #24785's fix merged. #24785 (found by the walled pass of round 3, QA run #24783) let a collab.mention notification reach a mentioned user who could not read the comment's record; its fix #24794 (merged e5b3de06a6) gates recipients on read access at the producer. The clause asserts that gate: read access decides, by id or email alike, and the author is never notified. Its live legs are the stock `single` sharing-excluded member and, on a walled boot, the cross-organization user; the pin reaches the engine layer only (RUNNER rule 6). acceptance[4] is unchanged: it scores who else's bell gains the row, this clause who among the mentioned may", |
| 2861 | + "ref": "#24864" |
2844 | 2862 | } |
2845 | 2863 | ] |
2846 | 2864 | }, |
|
0 commit comments