From d9acd21a35dc54409aeee6e8f58953910366d3ed Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 21:31:06 +0000 Subject: [PATCH 1/4] feat(plugin-audit)!: sys_comment declares no reactions column A reaction is its reactor's own sys_comment_reaction row; nothing about reactions is stored or computed on the comment row. The four generated translation bundles drop the field's label (check-i18n-bundles --write). Claude-Session: https://claude.ai/code/session_013LbZ9MhPp1iriZEtgJqioA Co-authored-by: Claude --- .../src/objects/sys-comment.object.ts | 23 ++++++++++++++----- .../src/translations/en.objects.generated.ts | 4 ---- .../translations/es-ES.objects.generated.ts | 4 ---- .../translations/ja-JP.objects.generated.ts | 4 ---- .../translations/zh-CN.objects.generated.ts | 4 ---- 5 files changed, 17 insertions(+), 22 deletions(-) diff --git a/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts b/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts index 534c4c1ad9a..5364816272b 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts @@ -43,6 +43,20 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; * Existing databases keep the two columns as unmanaged leftovers; there is no * migration (same disposition as #2755). * + * ## Retired: `reactions` (maintainer ruling A on #22505, as amended; #22573) + * + * `reactions` stored every emoji reaction on the comment's own row as one JSON + * map, so a member reacting to another member's comment had to update a row + * the author-only edit rule reserves for its author, and two members reacting + * at once overwrote each other. A reaction is now its reactor's own record, + * `sys_comment_reaction` (comment, emoji, user), and the column retires with + * NOTHING in its place on the comment row: no stored aggregate and no + * computed-on-read block. A reader that shows reactions reads the + * `sys_comment_reaction` rows of the comments on screen (`comment_id` `$in` + * their ids) and groups them by emoji. Reactions stored in the column are not + * converted into records; the D3 record is the semantic entry + * `sys-comment-reactions-column-retired`. + * * @namespace sys */ export const SysComment = ObjectSchema.create({ @@ -122,12 +136,9 @@ export const SysComment = ObjectSchema.create({ group: 'Body', }), - reactions: Field.textarea({ - label: 'Reactions', - required: false, - description: 'JSON object mapping each emoji to the list of user ids who reacted', - group: 'Body', - }), + // No `reactions` field: an emoji reaction is its reactor's own + // `sys_comment_reaction` row (#22566), and nothing about reactions is + // stored or computed on the comment row (#22573). // ── Lifecycle ──────────────────────────────────────────────── is_edited: Field.boolean({ diff --git a/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts index 030c6eb64cc..bdf7b9a9c1f 100644 --- a/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts @@ -217,10 +217,6 @@ export const enObjects: NonNullable = { label: "Mentions", help: "JSON array of the user ids @mentioned in the comment" }, - reactions: { - label: "Reactions", - help: "JSON object mapping each emoji to the list of user ids who reacted" - }, is_edited: { label: "Edited" }, diff --git a/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts index f7eca4e5a7e..6da3713902e 100644 --- a/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts @@ -217,10 +217,6 @@ export const esESObjects: NonNullable = { label: "Menciones", help: "Matriz JSON de los id de usuario mencionados con @ en el comentario." }, - reactions: { - label: "Reacciones", - help: "Objeto JSON que asigna cada emoji a la lista de id de usuario de quienes reaccionaron." - }, is_edited: { label: "Editado" }, diff --git a/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts index d1ea3c1cb74..6323eb5a04d 100644 --- a/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts @@ -217,10 +217,6 @@ export const jaJPObjects: NonNullable = { label: "メンション", help: "コメント内で @メンションされたユーザー ID の JSON 配列" }, - reactions: { - label: "リアクション", - help: "各絵文字を、リアクションしたユーザー ID のリストに対応付ける JSON オブジェクト" - }, is_edited: { label: "編集済み" }, diff --git a/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts index 4fbc6eb85dc..475f4520d3d 100644 --- a/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts @@ -217,10 +217,6 @@ export const zhCNObjects: NonNullable = { label: "提及", help: "评论中被 @ 提及的用户 ID 的 JSON 数组" }, - reactions: { - label: "回应", - help: "将每个表情映射到对其做出回应的用户 ID 列表的 JSON 对象" - }, is_edited: { label: "已编辑" }, From 330fc84c17e953b7e51cdb9dacf4eedf684d97b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 21:36:59 +0000 Subject: [PATCH 2/4] feat(spec,plugin-audit): D3 record for the sys_comment.reactions retirement, and its pins The semantic entry sys-comment-reactions-column-retired carries the prescription; the data door's INVALID_FIELD refusal carries the enforcement. Pins: sys_comment declares no reactions field and the record names it; a create or update naming the field is refused 400 INVALID_FIELD and writes nothing. Claude-Session: https://claude.ai/code/session_013LbZ9MhPp1iriZEtgJqioA Co-authored-by: Claude --- .../sys-comment-retired-fields.test.ts | 53 ++++++- ...comment-reactions-door.integration.test.ts | 132 ++++++++++++++++++ ...18.sys-comment-reactions-column-retired.ts | 55 ++++++++ 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 packages/plugins/plugin-audit/src/sys-comment-reactions-door.integration.test.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.sys-comment-reactions-column-retired.ts diff --git a/packages/plugins/plugin-audit/src/objects/sys-comment-retired-fields.test.ts b/packages/plugins/plugin-audit/src/objects/sys-comment-retired-fields.test.ts index ec9f3650b1f..77128fda6e1 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-comment-retired-fields.test.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-comment-retired-fields.test.ts @@ -1,7 +1,8 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect } from 'vitest'; -import { SysComment } from './index.js'; +import { MIGRATIONS_BY_MAJOR } from '@objectstack/spec/migrations'; +import { SysComment, SysCommentReaction } from './index.js'; /** * #4756 — `sys_comment.visibility` / `sys_comment.reply_count` are RETIRED @@ -72,3 +73,53 @@ describe('sys_comment — retired fields stay retired (#4756)', () => { expect(Object.keys(fields)).toContain('thread_id'); }); }); + +/** + * #22573 — `sys_comment.reactions` is RETIRED (maintainer ruling A on #22505, + * as amended: the column retires and its stored reactions are not migrated; + * the delegated judgment: no read-only aggregate takes its place). + * + * A reaction is its reactor's own `sys_comment_reaction` record, so nothing + * about reactions lives on the comment row, stored or computed on read. Unlike + * the two #4756 removals above, this one carries an ADR-0087 D3 record of its + * own, because a client really wrote the column: the semantic entry is what an + * upgrading author is shown, and the data door's `INVALID_FIELD` refusal is + * the enforcement (pinned in `sys-comment-reactions-door.integration.test.ts`). + */ +const REACTIONS_RECORD_ID = 'sys-comment-reactions-column-retired'; +const REACTIONS_PRESCRIPTION = + 'a reaction is its reactor\'s own `sys_comment_reaction` record; nothing about reactions is ' + + 'stored or computed on the comment row (ruling A on #22505, as amended; #22573)'; + +describe('sys_comment — reactions stays retired (#22573)', () => { + it('sys_comment declares no reactions field, and no index, highlight or title names it', () => { + const object = SysComment as { + fields?: Record; + indexes?: Array<{ fields?: string[] }>; + highlightFields?: string[]; + titleFormat?: string; + nameField?: string; + displayNameField?: string; + }; + expect( + Object.keys(object.fields ?? {}), + `sys_comment.reactions was retired — ${REACTIONS_PRESCRIPTION}`, + ).not.toContain('reactions'); + expect((object.indexes ?? []).flatMap((i) => i.fields ?? [])).not.toContain('reactions'); + expect(object.highlightFields ?? []).not.toContain('reactions'); + expect(object.titleFormat ?? '').not.toContain('reactions'); + expect([object.nameField, object.displayNameField]).not.toContain('reactions'); + }); + + it('the retirement record names it: a D3 semantic entry on the protocol-18 step', () => { + const entry = MIGRATIONS_BY_MAJOR[18]?.semantic.find((e) => e.id === REACTIONS_RECORD_ID); + expect(entry, `the ADR-0087 D3 record \`${REACTIONS_RECORD_ID}\` is missing from step 18`).toBeDefined(); + expect(entry!.surface.startsWith('sys_comment.reactions ')).toBe(true); + expect(entry!.replacement).toContain('sys_comment_reaction'); + }); + + it('keeps the replacement the record points at: one sys_comment_reaction row per comment, emoji and user', () => { + const fields = Object.keys((SysCommentReaction as { fields?: Record }).fields ?? {}); + expect(fields).toEqual(expect.arrayContaining(['comment_id', 'emoji', 'user_id'])); + }); +}); diff --git a/packages/plugins/plugin-audit/src/sys-comment-reactions-door.integration.test.ts b/packages/plugins/plugin-audit/src/sys-comment-reactions-door.integration.test.ts new file mode 100644 index 00000000000..bb5a7cb0627 --- /dev/null +++ b/packages/plugins/plugin-audit/src/sys-comment-reactions-door.integration.test.ts @@ -0,0 +1,132 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * A write that still names the retired `sys_comment.reactions` column is + * refused loudly (#22573, ruling A on #22505 as amended). + * + * The column retired with nothing in its place and no data migration, so the + * one question left for a client that still writes it — the console before + * its pin moved, or any other — is what the data door answers. Measured on a + * booted showcase over the REST door (`POST` and `PATCH + * /api/v1/data/sys_comment` carrying `reactions`): before the retirement both + * stored the map (201 / 200); after it both answer `400 INVALID_FIELD` naming + * the field, and nothing is written. This file pins that answer on the real + * engine, the real plugin and a real driver: the refusal is the engine's + * undeclared-field preflight, decided from `sys_comment`'s declared field map + * before any hook or driver runs, and `mapDataError` is the step the REST door + * takes from the thrown error to the wire. + * + * A silent drop would be the failure: a client would believe its reaction was + * saved. The assertions are on the envelope (`code`, `status`, the named + * field) and on the stored rows, never on a bare throw. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { ObjectKernel } from '@objectstack/core'; +import { ObjectQL, ObjectQLPlugin } from '@objectstack/objectql'; +import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm'; +import { mapDataError } from '@objectstack/types'; + +import { AuditPlugin } from './audit-plugin.js'; + +const COMMENT = 'sys_comment'; +/** A parent object every caller reads. */ +const OPEN = 'rr_board'; +const HARNESS_PACKAGE = 'com.objectstack.audit.test.comment-reactions-door'; + +const SYS = { isSystem: true } as const; +const AUTHOR = { userId: 'u_author', positions: ['org_member'] }; + +const openObject = { + name: OPEN, + label: 'Board', + fields: { name: { name: 'name', label: 'Name', type: 'text' as const } }, +}; + +type Row = Record; + +const refusal = async (run: () => Promise): Promise => { + try { + await run(); + } catch (e) { + return e; + } + return undefined; +}; + +describe('sys_comment — a write naming the retired reactions column is refused, not dropped (#22573)', () => { + let kernel: ObjectKernel; + let engine: ObjectQL; + let threadId: string; + let commentId: string; + + const REACTIONS = JSON.stringify({ '👍': [AUTHOR.userId] }); + + beforeAll(async () => { + kernel = new ObjectKernel({ logger: { level: 'silent' } }); + await kernel.use(new ObjectQLPlugin()); + await kernel.use(new AuditPlugin()); + await kernel.bootstrap(); + + engine = kernel.getService('objectql'); + const driver = new SqliteWasmDriver({ filename: ':memory:' }); + await driver.connect(); + engine.registerDriver(driver, true); + engine.registry.registerObject(openObject as any, HARNESS_PACKAGE); + await engine.syncSchemas(); + + const board = await engine.insert(OPEN, { name: 'board' }, { context: SYS }); + threadId = `${OPEN}:${board.id}`; + // The control: the same create without `reactions` goes through the door, + // so the refusals below are about the key and nothing else. + const created = await engine.insert( + COMMENT, + { thread_id: threadId, author_id: AUTHOR.userId, body: 'original' }, + { context: AUTHOR }, + ); + commentId = String(created.id); + }, 120_000); + + afterAll(async () => { + if (kernel) { + await Promise.race([kernel.shutdown(), new Promise((r) => setTimeout(r, 10_000))]); + } + }, 30_000); + + it('create: 400 INVALID_FIELD naming `reactions`, and no comment row is written', async () => { + const error = await refusal(() => + engine.insert( + COMMENT, + { thread_id: threadId, author_id: AUTHOR.userId, body: 'with reactions', reactions: REACTIONS }, + { context: AUTHOR }, + ), + ); + expect(error, 'a create carrying `reactions` was accepted').toBeDefined(); + const wire = mapDataError(error, COMMENT); + expect(wire.status).toBe(400); + expect(wire.body.code).toBe('INVALID_FIELD'); + expect(wire.body.field).toBe('reactions'); + + const rows = (await engine.find(COMMENT, { context: SYS })) as Row[]; + expect(rows.filter((r) => r.body === 'with reactions')).toHaveLength(0); + }); + + it('update: 400 INVALID_FIELD naming `reactions`, and the comment row is unchanged', async () => { + const error = await refusal(() => + engine.update( + COMMENT, + { body: 'edited', reactions: REACTIONS }, + { where: { id: commentId }, context: AUTHOR }, + ), + ); + expect(error, 'an update carrying `reactions` was accepted').toBeDefined(); + const wire = mapDataError(error, COMMENT); + expect(wire.status).toBe(400); + expect(wire.body.code).toBe('INVALID_FIELD'); + expect(wire.body.field).toBe('reactions'); + + const row = (await engine.findOne(COMMENT, { where: { id: commentId }, context: SYS })) as Row; + expect(row.body).toBe('original'); + expect(Object.keys(row)).not.toContain('reactions'); + }); +}); diff --git a/packages/spec/src/migrations/entries/semantic/18.sys-comment-reactions-column-retired.ts b/packages/spec/src/migrations/entries/semantic/18.sys-comment-reactions-column-retired.ts new file mode 100644 index 00000000000..06c3e863074 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.sys-comment-reactions-column-retired.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// #22573 (ruling A on #22505, as amended) — a platform-object COLUMN +// retirement, not a spec-key retirement: no authorable spec key moves, so +// nothing lands in RETIRED_KEYS_BY_MAJOR and no D2 conversion exists to pair +// with (the ups-delegated-from-column-retired shape). The data door's own +// refusal of an undeclared field is the enforced channel; this entry carries +// the prescription. By the maintainer's word there is no data migration. +export const entry: SemanticMigration = { + id: 'sys-comment-reactions-column-retired', + // No backticks in `surface` — build-upgrade-guide.ts renders it inside a + // code span AND a table cell. + surface: + 'sys_comment.reactions — the JSON textarea column that held every emoji reaction to a comment ' + + 'as one map of emoji to user ids, on the comment row itself, left the platform comment table ' + + 'declared by plugin-audit (packages/plugins/plugin-audit/src/objects/sys-comment.object.ts), ' + + 'together with its label in the four generated translation bundles', + replacement: + 'a reaction is its reactor\'s own `sys_comment_reaction` record (`comment_id`, `emoji`, ' + + '`user_id`): a member reacts by creating that row and un-reacts by deleting it. Nothing takes ' + + 'the column\'s place on the comment row, neither a stored aggregate nor a block computed on ' + + 'read. A screen that showed reactions reads the `sys_comment_reaction` rows of the comments it ' + + 'shows in one request (`comment_id` `$in` their ids) and groups them by comment and emoji; a ' + + 'client that rewrote the column writes the member\'s own reaction row instead. An application ' + + 'that shows reactions grants `sys_comment_reaction` read, create and delete beside its ' + + '`sys_comment` grant', + reason: + 'Maintainer ruling 2026-10-10: a reaction is the reactor\'s own record, the column retires, and ' + + 'reactions stored in it are not migrated; the delegated judgment on the same day: no read-only ' + + 'aggregate replaces it. The column made every reaction an update of the comment\'s row, so a ' + + 'member reacting to another member\'s comment needed a write the author-only edit rule on ' + + '`sys_comment` refuses, and two members reacting at once overwrote each other\'s whole map. ' + + 'One row per reaction settles both by construction. An aggregate column was not kept because ' + + 'no declared field type holds the grouped map (a summary roll-up is scalar), so keeping one ' + + 'current would need a privileged writer on the author\'s row; a block computed on read has no ' + + 'seam, because comments are served by the generic data door. Census of the column\'s users: in ' + + 'this repository the field declaration and its four translation labels were the only ' + + 'references; the one client that read and wrote it was the console\'s record chatter, which ' + + 'reads and writes `sys_comment_reaction` rows wherever the deployment declares that object, as ' + + 'this framework does. Existing databases: schema sync is additive, so the physical column ' + + 'stays with the values it holds, the boot drift report names it orphaned, and the boot is ' + + 'otherwise clean; the data door neither returns nor accepts it. Those stored reactions are not ' + + 'converted into records and do not reappear. The operator drops the column with os migrate ' + + 'apply --allow-destructive, the remedy the drift report names.', + acceptanceCriteria: + 'No client write and no authored seed row, filter or list-view column names `reactions` on ' + + '`sys_comment`: the data door answers a create or an update that carries it, and a list filter ' + + 'on it, with 400 INVALID_FIELD naming the field, and a read of a comment no longer returns it. ' + + 'A screen that shows reactions reads `sys_comment_reaction` rows, and its users hold read, ' + + 'create and delete on that object. After os migrate apply --allow-destructive, run with the ' + + 'audit plugin composed (an auth secret resolves), the boot no longer reports the orphaned ' + + 'column.', +}; From b95f4487ced348b93c0396ee508ea300d481d02a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 21:39:32 +0000 Subject: [PATCH 3/4] docs(changeset): sys_comment.reactions retirement, with its migration and the operator sentence The sibling changeset that added sys_comment_reaction said the column was unchanged in this release; both are pending stock for the same release, so that bullet now points at the retirement instead. Claude-Session: https://claude.ai/code/session_013LbZ9MhPp1iriZEtgJqioA Co-authored-by: Claude --- .changeset/22566-comment-reaction-record.md | 2 +- .../22573-sys-comment-reactions-retired.md | 30 +++++++++++++++++++ .../src/objects/sys-comment.object.ts | 6 +++- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 .changeset/22573-sys-comment-reactions-retired.md diff --git a/.changeset/22566-comment-reaction-record.md b/.changeset/22566-comment-reaction-record.md index d5ba5fd69f4..2a8cdf719e5 100644 --- a/.changeset/22566-comment-reaction-record.md +++ b/.changeset/22566-comment-reaction-record.md @@ -14,4 +14,4 @@ Clause-②: yes (widening) - **Reading reactions for a feed.** Read the reactions of the comments on screen in one request, `comment_id` `$in` those ids, and group the rows by comment and emoji. Nothing is stored or computed on the comment row. - **Granting it.** The object's create, read and delete bits come from a permission set, as `sys_comment`'s do. The `everyone`-bound baseline (`member_default`) grants nothing on it. - **`@objectstack/spec`: the platform object registry lists it.** `PLATFORM_OBJECTS_BY_PACKAGE['plugin-audit']` now includes `sys_comment_reaction`, and so do `PLATFORM_PROVIDED_OBJECT_NAMES` and `isPlatformProvidedObjectName`. A stack that references the object is no longer reported as naming an unknown platform object. This is the only change in the package. -- **`sys_comment.reactions` is unchanged in this release.** The column is still declared and still written by clients that write it. It is retired separately, and reactions stored in it are not converted into records. +- **`sys_comment.reactions` is retired by a separate change on this line**, which carries its own entry. Reactions stored in that column are not converted into records. diff --git a/.changeset/22573-sys-comment-reactions-retired.md b/.changeset/22573-sys-comment-reactions-retired.md new file mode 100644 index 00000000000..b72455dd072 --- /dev/null +++ b/.changeset/22573-sys-comment-reactions-retired.md @@ -0,0 +1,30 @@ +--- +'@objectstack/plugin-audit': minor +'@objectstack/spec': minor +--- + +feat(plugin-audit)!: `sys_comment.reactions` retires; an emoji reaction lives only in `sys_comment_reaction`, and reactions stored in the old column are not migrated + +Clause-②: no (narrowing) + + + +**BREAKING**, shipped as `minor` under the repo's convention for breaking changes on this line. + +**Existing emoji reactions stored in `sys_comment.reactions` are gone after the upgrade.** They are not converted into `sys_comment_reaction` records, and no screen or API returns them again. Reactions now live in `sys_comment_reaction`, one row per comment, emoji and user, the reacting user's own record. + +- **`sys_comment`** (`@objectstack/plugin-audit`) no longer declares `reactions`. Nothing takes its place on the comment row: no stored aggregate, and no block computed on read. Its label leaves the four generated translation bundles. +- **The data door refuses the field.** A create or an update of a comment that still carries `reactions`, and a list filter on it, answer `400 INVALID_FIELD` naming the field, and nothing is written. The rest of the payload is not applied either. A read of a comment no longer returns `reactions`. +- **`@objectstack/spec`**: the protocol-18 step of the ADR-0087 migration chain gains the semantic entry `sys-comment-reactions-column-retired`, so `os migrate meta` and the upgrade guide carry the prescription below. + +**What moves for consumers.** + +| You wrote | Write instead | +|:---|:---| +| `sys_comment.reactions` read as a map of emoji to user ids | one read of `sys_comment_reaction` for the comments on screen (`comment_id` `$in` their ids), grouped by comment and emoji | +| an update of `sys_comment` that rewrites `reactions` to add or remove a user | create the user's own `sys_comment_reaction` row (`comment_id`, `emoji`) to react; delete that row to un-react | +| `reactions` in a seed row, filter or list-view column on `sys_comment` | delete it | + +An application that shows reactions grants `sys_comment_reaction` read, create and delete beside its `sys_comment` grant; without it a member cannot react. The console's record chatter already reads and writes `sys_comment_reaction` rows wherever the deployment declares that object, as this framework does. + +**Existing databases — nothing moves automatically.** Schema sync is additive: the physical `reactions` column stays with the values it holds, the boot drift report names it orphaned (`sys_comment.reactions: column exists in the database but not in metadata`), and the boot is otherwise clean. The data door neither returns nor accepts the column. To drop it, run `os migrate apply --allow-destructive` with the audit plugin composed (an auth secret resolves); the stored reactions go with it. diff --git a/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts b/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts index 5364816272b..405c8c5de43 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-comment.object.ts @@ -55,7 +55,11 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; * `sys_comment_reaction` rows of the comments on screen (`comment_id` `$in` * their ids) and groups them by emoji. Reactions stored in the column are not * converted into records; the D3 record is the semantic entry - * `sys-comment-reactions-column-retired`. + * `sys-comment-reactions-column-retired`. An existing database keeps the + * physical column and its values (schema sync is additive): the boot drift + * report names it orphaned, the data door neither returns nor accepts it + * (`400 INVALID_FIELD` on a write that names it), and + * `os migrate apply --allow-destructive` drops it. * * @namespace sys */ From f2825700c6a7ef2a18cf21ab5fa9cd167b6cee5b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 21:47:55 +0000 Subject: [PATCH 4/4] docs(changeset): leave the sibling release note as it stands; supersede its column line from this entry check-empty-changeset refuses a change to a changeset this PR did not add, and a deliberate correction there needs a person's confirmation. The stale line is reported to the seat instead, and this entry says plainly that it supersedes it. Claude-Session: https://claude.ai/code/session_013LbZ9MhPp1iriZEtgJqioA Co-authored-by: Claude --- .changeset/22566-comment-reaction-record.md | 2 +- .changeset/22573-sys-comment-reactions-retired.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/22566-comment-reaction-record.md b/.changeset/22566-comment-reaction-record.md index 2a8cdf719e5..d5ba5fd69f4 100644 --- a/.changeset/22566-comment-reaction-record.md +++ b/.changeset/22566-comment-reaction-record.md @@ -14,4 +14,4 @@ Clause-②: yes (widening) - **Reading reactions for a feed.** Read the reactions of the comments on screen in one request, `comment_id` `$in` those ids, and group the rows by comment and emoji. Nothing is stored or computed on the comment row. - **Granting it.** The object's create, read and delete bits come from a permission set, as `sys_comment`'s do. The `everyone`-bound baseline (`member_default`) grants nothing on it. - **`@objectstack/spec`: the platform object registry lists it.** `PLATFORM_OBJECTS_BY_PACKAGE['plugin-audit']` now includes `sys_comment_reaction`, and so do `PLATFORM_PROVIDED_OBJECT_NAMES` and `isPlatformProvidedObjectName`. A stack that references the object is no longer reported as naming an unknown platform object. This is the only change in the package. -- **`sys_comment.reactions` is retired by a separate change on this line**, which carries its own entry. Reactions stored in that column are not converted into records. +- **`sys_comment.reactions` is unchanged in this release.** The column is still declared and still written by clients that write it. It is retired separately, and reactions stored in it are not converted into records. diff --git a/.changeset/22573-sys-comment-reactions-retired.md b/.changeset/22573-sys-comment-reactions-retired.md index b72455dd072..42d78ed00f4 100644 --- a/.changeset/22573-sys-comment-reactions-retired.md +++ b/.changeset/22573-sys-comment-reactions-retired.md @@ -11,7 +11,7 @@ Clause-②: no (narrowing) **BREAKING**, shipped as `minor` under the repo's convention for breaking changes on this line. -**Existing emoji reactions stored in `sys_comment.reactions` are gone after the upgrade.** They are not converted into `sys_comment_reaction` records, and no screen or API returns them again. Reactions now live in `sys_comment_reaction`, one row per comment, emoji and user, the reacting user's own record. +**Existing emoji reactions stored in `sys_comment.reactions` are gone after the upgrade.** They are not converted into `sys_comment_reaction` records, and no screen or API returns them again. Reactions now live in `sys_comment_reaction`, one row per comment, emoji and user, the reacting user's own record. Where the entry that introduces `sys_comment_reaction` says the column is unchanged, this entry supersedes it: the column retires. - **`sys_comment`** (`@objectstack/plugin-audit`) no longer declares `reactions`. Nothing takes its place on the comment row: no stored aggregate, and no block computed on read. Its label leaves the four generated translation bundles. - **The data door refuses the field.** A create or an update of a comment that still carries `reactions`, and a list filter on it, answer `400 INVALID_FIELD` naming the field, and nothing is written. The rest of the payload is not applied either. A read of a comment no longer returns `reactions`.