You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Record feed: the reaction chip removes the caller's own reaction through the unreact door, so a member with no delete grant can take a reaction back (ui half of #26161) #26346
Filed by the domain:server seat 1, the holding seat of #26161, as the ruling there directs: "The server card lands first; the holding seat files the domain:ui sub-card with Blocked-by: this card" (ruling 6422861164, letter A, maintainer 「同意」 2026-10-26T17:27Z). ⛔ Not a claim.
Part of #26161
Blocked-by: #26161
Ruling-ref: 6422861164
Symptom. A member who holds no position can add a comment reaction but never take it back. The console's reaction chip removes through the generic DELETE /api/v1/data/sys_comment_reaction/:id, which needs an object-level delete grant. No every-member set may carry that grant (ADR-0090 D5, security-anchor-high-privilege), so the chip offers no remove at all: feedReactionRemove reads a delete grant.
The server half (PR #26345, #26161).AuditPlugin mounts an own-row unreact door:
DELETE /api/v1/comments/reactions/:id, where :id is the sys_comment_reaction row id. It reads no body, and any query parameter is refused. Answers:
200{ success: true, data: { id, deleted: true } }: the caller's own reaction is removed;
400 VALIDATION_ERROR: a query parameter, or no id;
401 UNAUTHENTICATED;
403 with a held sign-in's own code;
403 PERMISSION_DENIED: another member's reaction, which stays;
404 RECORD_NOT_FOUND: no such row, or one the caller cannot read;
503 SERVICE_UNAVAILABLE.
@objectstack/client has no method for it yet.
The UI half (this card, ui S):
The reaction chip (ui/packages/app-shell/src/views/RecordDetailView.tsx, reactionRemove near :435) removes the caller's own reaction through the new route instead of the generic data door.
feedReactionRemove in ui/packages/core/src/utils/affordanceGrants.ts:213 stops reading a delete grant on sys_comment_reaction. Every member who may react may take back their own reaction, so the affordance is "this reaction is mine" (created_by is the caller), not a grant. Update the affordance pins (RecordDetailView.feedGrant-12082.test.tsx, affordanceGrantMap-12082.test.tsx) to match.
If a client method is wanted, adding one to @objectstack/client (with its route-ledger row) belongs here too.
Done when: as a plain member with member_default and no position, in the console, add a reaction and click your own chip to remove it (200, and the chip goes away). Another member's chip offers no remove. A member with a delete grant sees the same behaviour.
Claim: PM loop round 1
Session: session_017iXjUm1rujwDk7f3KT1Pkk (seat); implementing card thread session_01ADHkmPysaCwFvT4Pqb88z8
Account: os-steve
Branch: claude/project-thread-6tmyk6 (platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)
Worktree: the card thread's own container
Domain: domain:ui
Seat: domain:ui#1
File surface: ui/packages/app-shell/src/views/RecordDetailView.tsx (reaction remove path), ui/packages/core/src/utils/affordanceGrants.ts (feedReactionRemove), the two affordance pins RecordDetailView.feedGrant-12082.test.tsx and affordanceGrantMap-12082.test.tsx, one new regression test, re-recorded scripts/sdui-manifest.sources/core.json, and a root changeset. Out of bounds: ObjectView.tsx and objectView*.ts (#25992), seat 2 plugin packages, and a new @objectstack/client method (stop → needs_decision) (stop on breach; explain in the report). ⛔ ui/.github/** and the root .github/workflows/ci.yml are out of bounds (maintainer's word, ui-merge epic in flight). Changeset in the root .changeset/.
Container & model: small, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
Clause-②: no (the new route changes which rows offer a remove, not an exported type; re-grade if the affordance map row's type changes shape or a new export appears)
Metadata-protocol: none
Responsibility: n/a: renderer defect in ui/, the producing code is the cited ui file; no platform path covers it; reached through the public Console
Thread-read: none
Premise: holds (read-only premise check at origin/main6390fe5, 2026-10-27T00:56Z: #26161 closed, PR #26345 merged with the own-row unreact door; feedReactionRemove at ui/packages/core/src/utils/affordanceGrants.ts still reads { crud: delete, grant: delete }; spot-checked by the seat)
Serial constraints cleared: core.json is also touched by open #26367 and #26343; the seat sets the enqueue order and asks for a merge of main plus a re-record at the turn
Dispatch order handed to the card thread by the project coordinator (three partitions: ruling / mechanism hypotheses / suggested route); the dev does not claim, and starts only once this claim names its branch.
{
"issue": 26346,
"status": "done",
"branch": "claude/project-thread-6tmyk6",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26384",
"session": "session_01ADHkmPysaCwFvT4Pqb88z8",
"premise_still_valid": true,
"summary": "The record feed's take-back of the caller's own reaction now calls the unreact door, DELETE /api/v1/comments/reactions/:id, through the page's existing authFetch and VITE_SERVER_URL base (no body, no query, credentials include); a non-2xx answer is a refused write and the existing settle step puts the reaction back with one toast. feedReactionRemove in AFFORDANCE_GRANTS changes VALUE only, from { crud: 'delete', grant: 'delete' } to { crud: null, grant: 'create' }: the row type keeps its shape, ConsoleAffordance keeps its member set, and no export is added or removed (Clause-② no stands). Which chip offers a take-back is unchanged: the handler already removes only the row id the read or the create recorded for the caller (rows carry id and the server-stamped user_id), and a click on another member's chip adds the caller's own row. No @objectstack/client method added. Card assignee is empty, so the PR carries no assignee (relay read-back: none).",
"tests": "Head 0a064d80. Lock run: pnpm --dir ui exec vitest run --maxWorkers=2 packages/app-shell/src/views/RecordDetailView.reaction packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx packages/plugin-form/src/affordanceGrantMap-12082.test.tsx packages/core/src/utils/ -> VERDICT command-exit 0, Test Files 112 passed (112), Tests 2011 passed (2011). Narrowed: app-shell's full suite (~1725 files) is left to CI Test UI; the narrowing is every RecordDetailView reaction file, the feed pin, the map pin and core utils. Ablation (security label, seat ask): the source change committed first; RecordDetailView.tsx and affordanceGrants.ts restored to BASE f02e6443 (on-disk proof: COMMENT_REACTION_REMOVE_PATH count 0, old dataSource.delete(COMMENT_REACTION_OBJECT) count 1, old feedReactionRemove delete row count 1); ui vitest aliases @object-ui/core to src, so no build leg. Feed + map pins: VERDICT command-exit 1, Tests 8 failed | 69 passed (77): the 3 map-pin rows (grant expectation, create-only shape, write census), the 2 rewritten take-back cases, the registered-schema case, and 2 of the 3 new cases (own add-then-take-back; refused take-back). The third new case (another member's chip offers no take-back) stays green on BASE by design: that behaviour did not move. Restore: git checkout HEAD -- both paths, git diff HEAD empty, both blob hashes equal HEAD's. Type-check: @object-ui/core type-check exit 0; @object-ui/app-shell type-check (tsc --noEmit and tsc -p tsconfig.test.json) exit 0 after building its dependency closure; @object-ui/plugin-form type-check exit 0.",
"gates": "Head 0a064d80. dispatch-gates --commands derived 35 stdout commands; all 35 run, each exit 0 (the check-adr-0087-registration, changeset-no-major, closing-keyword-parity, comment-mask-corpus, empty-changeset, sdui-manifest, undeclared-dep-imports pairs; release-rehearsal-clone and release-pending-publish self-tests; check:duration-unit-keys, changeset-gate-self-tests, closed-ceremony-text, cross-package-test-inputs, dispatcher-error-vocabulary, driver-memory-census, engine-double-contract, gitlink-declared, nul-bytes, pm-changeset-deadline-census, published-files, query-options-erasure, refd-timer-probe, skill-identifier-liveness, test-source-alias, tier-file-adoption, type-check-coverage, type-check-debt, watch-hint-literal, where-matcher). The first --ran reconciliation named 10 more derived families UNRUN; all 10 run, each exit 0 (check-ci-filter-parity, check-scripts-symbol-anchors and its --self-test, check:agent-test-spelling, bash32-floor, cli-command-ids, entry-guard, parse-guard, pnpm-filter-targets, pnpm-flag-spellings). Final: dispatch-gates --ran exit 0, '45 derived famil(ies) accounted for — 45 run, 0 NOT-MEASURED'. sdui: node scripts/gen-sdui-manifest-node.mjs under the lock, VERDICT command-exit 0, '1 of 26 registry package record(s) re-recorded: core', sdui.manifest.json unchanged. PR check-runs: in_progress at report time (not waited on).",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "4 — all reads: mcp__github__issue_read x3 (card get, card comments twice), mcp__github__pull_request_read x1 (get_files on #26345). No MCP write.",
"api_writes": "2 — relay pr_create (draft #26384, request fw-20261027T013640Z-d4f107) and this os-dev-report comment via post-stamped.mjs. git push is not a REST write.",
"line_budget": "8 files, +191 / -45 (source 2 files +38/-14; tests 3 files +130/-22; docs 1, changeset 1, sdui record 1)",
"deviations": [
"A third forced test edit beyond the dispatch's two: RecordDetailView.reactionRecords-12078.test.tsx asserted the take-back as dataSource.delete(sys_comment_reaction, id); its fake server now models the unreact door per member (own row removed, 403 otherwise) and the two take-back cases assert the removed ids at the door. Named in the PR body.",
"ui/content/docs/plugins/plugin-detail.mdx edited (one sentence said the take-back needs the delete grant): a docs file outside the dispatch's file list, kept to that sentence per ui/AGENTS.md #2.",
"The new regression cases live in a new describe block inside RecordDetailView.feedGrant-12082.test.tsx rather than a new file, to reuse that file's mount scaffolding.",
"The first push was refused by the pre-push check:commit-card-trailers (harness trailer named a model); the unpublished commit was reworded to the model-free pair before any push landed."
],
"files_changed": [
".changeset/26346-reaction-unreact-door.md",
"scripts/sdui-manifest.sources/core.json",
"ui/content/docs/plugins/plugin-detail.mdx",
"ui/packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx",
"ui/packages/app-shell/src/views/RecordDetailView.reactionRecords-12078.test.tsx",
"ui/packages/app-shell/src/views/RecordDetailView.tsx",
"ui/packages/core/src/utils/affordanceGrants.ts",
"ui/packages/plugin-form/src/affordanceGrantMap-12082.test.tsx"
],
"open_questions": [
{
"question": "The door authorizes on the row's created_by; the chip decides 'mine' on the row's user_id. Both are stamped server-side from the reacting session at create, so they agree for every row made through the data door. Re-key the chip on created_by?",
"options": ["A keep user_id (this PR): the read's grouping and the take-back key on one column, and a disagreement would only surface as a refused take-back that the settle step already rolls back with a toast", "B key the own-row map on created_by in a follow-up, leaving the reactor display on user_id"],
"recommendation": "A, because the two columns name the same member by construction and B splits one read into two notions of 'mine'"
},
{
"question": "feedReactionRemove now asks grant 'create' (crud null). The control is handed over only under feedReactionAdd, so on the records store the take-back is decided by ownership alone. Keep the row, or retire it?",
"options": ["A keep the row with { crud: null, grant: 'create' } (this PR): no exported type moves, Clause-② stays no", "B delete the row and gate on ownership alone: narrows the exported ConsoleAffordance union, a Clause-② re-grade"],
"recommendation": "A, because it meets the card with no contract change"
}
],
"out_of_scope_findings": []
}
Activity
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1
Session:
session_017iXjUm1rujwDk7f3KT1Pkk(seat); implementing card threadsession_01ADHkmPysaCwFvT4Pqb88z8Account:
os-steveBranch:
claude/project-thread-6tmyk6(platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)Worktree: the card thread's own container
Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/app-shell/src/views/RecordDetailView.tsx(reaction remove path),ui/packages/core/src/utils/affordanceGrants.ts(feedReactionRemove), the two affordance pinsRecordDetailView.feedGrant-12082.test.tsxandaffordanceGrantMap-12082.test.tsx, one new regression test, re-recordedscripts/sdui-manifest.sources/core.json, and a root changeset. Out of bounds:ObjectView.tsxandobjectView*.ts(#25992), seat 2 plugin packages, and a new@objectstack/clientmethod (stop → needs_decision) (stop on breach; explain in the report). ⛔ui/.github/**and the root.github/workflows/ci.ymlare out of bounds (maintainer's word, ui-merge epic in flight). Changeset in the root.changeset/.Container & model:
small,mode:thread,model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate),effort: mediumClause-②:
no(the new route changes which rows offer a remove, not an exported type; re-grade if the affordance map row's type changes shape or a new export appears)Metadata-protocol: none
Responsibility:
n/a: renderer defect in ui/, the producing code is the cited ui file; no platform path covers it; reached through the public ConsoleThread-read: none
Premise: holds (read-only premise check at
origin/main6390fe5, 2026-10-27T00:56Z: #26161 closed, PR #26345 merged with the own-row unreact door;feedReactionRemoveatui/packages/core/src/utils/affordanceGrants.tsstill reads{ crud: delete, grant: delete }; spot-checked by the seat)Serial constraints cleared:
core.jsonis also touched by open #26367 and #26343; the seat sets the enqueue order and asks for a merge of main plus a re-record at the turnDispatch order handed to the card thread by the project coordinator (three partitions: ruling / mechanism hypotheses / suggested route); the dev does not claim, and starts only once this claim names its branch.
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 26346, "status": "done", "branch": "claude/project-thread-6tmyk6", "pr": "https://github.com/objectstack-ai/objectstack/pull/26384", "session": "session_01ADHkmPysaCwFvT4Pqb88z8", "premise_still_valid": true, "summary": "The record feed's take-back of the caller's own reaction now calls the unreact door, DELETE /api/v1/comments/reactions/:id, through the page's existing authFetch and VITE_SERVER_URL base (no body, no query, credentials include); a non-2xx answer is a refused write and the existing settle step puts the reaction back with one toast. feedReactionRemove in AFFORDANCE_GRANTS changes VALUE only, from { crud: 'delete', grant: 'delete' } to { crud: null, grant: 'create' }: the row type keeps its shape, ConsoleAffordance keeps its member set, and no export is added or removed (Clause-② no stands). Which chip offers a take-back is unchanged: the handler already removes only the row id the read or the create recorded for the caller (rows carry id and the server-stamped user_id), and a click on another member's chip adds the caller's own row. No @objectstack/client method added. Card assignee is empty, so the PR carries no assignee (relay read-back: none).", "tests": "Head 0a064d80. Lock run: pnpm --dir ui exec vitest run --maxWorkers=2 packages/app-shell/src/views/RecordDetailView.reaction packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx packages/plugin-form/src/affordanceGrantMap-12082.test.tsx packages/core/src/utils/ -> VERDICT command-exit 0, Test Files 112 passed (112), Tests 2011 passed (2011). Narrowed: app-shell's full suite (~1725 files) is left to CI Test UI; the narrowing is every RecordDetailView reaction file, the feed pin, the map pin and core utils. Ablation (security label, seat ask): the source change committed first; RecordDetailView.tsx and affordanceGrants.ts restored to BASE f02e6443 (on-disk proof: COMMENT_REACTION_REMOVE_PATH count 0, old dataSource.delete(COMMENT_REACTION_OBJECT) count 1, old feedReactionRemove delete row count 1); ui vitest aliases @object-ui/core to src, so no build leg. Feed + map pins: VERDICT command-exit 1, Tests 8 failed | 69 passed (77): the 3 map-pin rows (grant expectation, create-only shape, write census), the 2 rewritten take-back cases, the registered-schema case, and 2 of the 3 new cases (own add-then-take-back; refused take-back). The third new case (another member's chip offers no take-back) stays green on BASE by design: that behaviour did not move. Restore: git checkout HEAD -- both paths, git diff HEAD empty, both blob hashes equal HEAD's. Type-check: @object-ui/core type-check exit 0; @object-ui/app-shell type-check (tsc --noEmit and tsc -p tsconfig.test.json) exit 0 after building its dependency closure; @object-ui/plugin-form type-check exit 0.", "gates": "Head 0a064d80. dispatch-gates --commands derived 35 stdout commands; all 35 run, each exit 0 (the check-adr-0087-registration, changeset-no-major, closing-keyword-parity, comment-mask-corpus, empty-changeset, sdui-manifest, undeclared-dep-imports pairs; release-rehearsal-clone and release-pending-publish self-tests; check:duration-unit-keys, changeset-gate-self-tests, closed-ceremony-text, cross-package-test-inputs, dispatcher-error-vocabulary, driver-memory-census, engine-double-contract, gitlink-declared, nul-bytes, pm-changeset-deadline-census, published-files, query-options-erasure, refd-timer-probe, skill-identifier-liveness, test-source-alias, tier-file-adoption, type-check-coverage, type-check-debt, watch-hint-literal, where-matcher). The first --ran reconciliation named 10 more derived families UNRUN; all 10 run, each exit 0 (check-ci-filter-parity, check-scripts-symbol-anchors and its --self-test, check:agent-test-spelling, bash32-floor, cli-command-ids, entry-guard, parse-guard, pnpm-filter-targets, pnpm-flag-spellings). Final: dispatch-gates --ran exit 0, '45 derived famil(ies) accounted for — 45 run, 0 NOT-MEASURED'. sdui: node scripts/gen-sdui-manifest-node.mjs under the lock, VERDICT command-exit 0, '1 of 26 registry package record(s) re-recorded: core', sdui.manifest.json unchanged. PR check-runs: in_progress at report time (not waited on).", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "4 — all reads: mcp__github__issue_read x3 (card get, card comments twice), mcp__github__pull_request_read x1 (get_files on #26345). No MCP write.", "api_writes": "2 — relay pr_create (draft #26384, request fw-20261027T013640Z-d4f107) and this os-dev-report comment via post-stamped.mjs. git push is not a REST write.", "line_budget": "8 files, +191 / -45 (source 2 files +38/-14; tests 3 files +130/-22; docs 1, changeset 1, sdui record 1)", "deviations": [ "A third forced test edit beyond the dispatch's two: RecordDetailView.reactionRecords-12078.test.tsx asserted the take-back as dataSource.delete(sys_comment_reaction, id); its fake server now models the unreact door per member (own row removed, 403 otherwise) and the two take-back cases assert the removed ids at the door. Named in the PR body.", "ui/content/docs/plugins/plugin-detail.mdx edited (one sentence said the take-back needs the delete grant): a docs file outside the dispatch's file list, kept to that sentence per ui/AGENTS.md #2.", "The new regression cases live in a new describe block inside RecordDetailView.feedGrant-12082.test.tsx rather than a new file, to reuse that file's mount scaffolding.", "The first push was refused by the pre-push check:commit-card-trailers (harness trailer named a model); the unpublished commit was reworded to the model-free pair before any push landed." ], "files_changed": [ ".changeset/26346-reaction-unreact-door.md", "scripts/sdui-manifest.sources/core.json", "ui/content/docs/plugins/plugin-detail.mdx", "ui/packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx", "ui/packages/app-shell/src/views/RecordDetailView.reactionRecords-12078.test.tsx", "ui/packages/app-shell/src/views/RecordDetailView.tsx", "ui/packages/core/src/utils/affordanceGrants.ts", "ui/packages/plugin-form/src/affordanceGrantMap-12082.test.tsx" ], "open_questions": [ { "question": "The door authorizes on the row's created_by; the chip decides 'mine' on the row's user_id. Both are stamped server-side from the reacting session at create, so they agree for every row made through the data door. Re-key the chip on created_by?", "options": ["A keep user_id (this PR): the read's grouping and the take-back key on one column, and a disagreement would only surface as a refused take-back that the settle step already rolls back with a toast", "B key the own-row map on created_by in a follow-up, leaving the reactor display on user_id"], "recommendation": "A, because the two columns name the same member by construction and B splits one read into two notions of 'mine'" }, { "question": "feedReactionRemove now asks grant 'create' (crud null). The control is handed over only under feedReactionAdd, so on the records store the take-back is decided by ownership alone. Keep the row, or retire it?", "options": ["A keep the row with { crud: null, grant: 'create' } (this PR): no exported type moves, Clause-② stays no", "B delete the row and gate on ownership alone: narrows the exported ConsoleAffordance union, a Clause-② re-grade"], "recommendation": "A, because it meets the card with no contract change" } ], "out_of_scope_findings": [] }Generated by Claude Code