From 7eb5826ad26f1f89c7a51e77ffc527ab83c78973 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 26 Jul 2026 15:35:03 +0800 Subject: [PATCH] chore(i18n): purge dead sys_webhook_delivery bundle block + roll out bundle-ownership guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the stale sys_webhook_delivery translation block from the four plugin-webhooks i18n bundles (object removed in ADR-0018 M3 → sys_http_delivery); surgical, the sys_webhook block is untouched. - Fix three stale sys_webhook_delivery doc comments (platform-objects integration/index.ts + setup.app.ts, plugin-webhooks sys-webhook.object.ts). - Add the bundle-ownership guard test (#2834 5 / ADR-0029 D8) to the 8 packages that own i18n bundles, so a stray object block in a generated bundle fails the build instead of dying silently on the next `os i18n extract`. - Fix a live-object omission the guard caught: re-add SysCapability to plugin-security's extract config (it had curated translations in the bundles but had been dropped from the config), preserving the strings instead of deleting them. Co-Authored-By: Claude Opus 4.8 --- ...livery-i18n-and-bundle-ownership-guards.md | 25 ++++ .../platform-objects/src/apps/setup.app.ts | 2 +- .../platform-objects/src/integration/index.ts | 6 +- .../src/translations/bundle-ownership.test.ts | 48 ++++++++ .../src/translations/bundle-ownership.test.ts | 43 +++++++ .../scripts/i18n-extract.config.ts | 8 +- .../src/translations/bundle-ownership.test.ts | 52 ++++++++ .../src/translations/bundle-ownership.test.ts | 43 +++++++ .../plugin-webhooks/src/sys-webhook.object.ts | 9 +- .../src/translations/bundle-ownership.test.ts | 41 +++++++ .../src/translations/en.objects.generated.ts | 111 ------------------ .../translations/es-ES.objects.generated.ts | 111 ------------------ .../translations/ja-JP.objects.generated.ts | 111 ------------------ .../translations/zh-CN.objects.generated.ts | 111 ------------------ .../src/translations/bundle-ownership.test.ts | 47 ++++++++ .../src/translations/bundle-ownership.test.ts | 41 +++++++ .../src/translations/bundle-ownership.test.ts | 42 +++++++ 17 files changed, 396 insertions(+), 455 deletions(-) create mode 100644 .changeset/purge-webhook-delivery-i18n-and-bundle-ownership-guards.md create mode 100644 packages/plugins/plugin-approvals/src/translations/bundle-ownership.test.ts create mode 100644 packages/plugins/plugin-audit/src/translations/bundle-ownership.test.ts create mode 100644 packages/plugins/plugin-security/src/translations/bundle-ownership.test.ts create mode 100644 packages/plugins/plugin-sharing/src/translations/bundle-ownership.test.ts create mode 100644 packages/plugins/plugin-webhooks/src/translations/bundle-ownership.test.ts create mode 100644 packages/services/service-messaging/src/translations/bundle-ownership.test.ts create mode 100644 packages/services/service-realtime/src/translations/bundle-ownership.test.ts create mode 100644 packages/services/service-storage/src/translations/bundle-ownership.test.ts diff --git a/.changeset/purge-webhook-delivery-i18n-and-bundle-ownership-guards.md b/.changeset/purge-webhook-delivery-i18n-and-bundle-ownership-guards.md new file mode 100644 index 0000000000..63c386a1b2 --- /dev/null +++ b/.changeset/purge-webhook-delivery-i18n-and-bundle-ownership-guards.md @@ -0,0 +1,25 @@ +--- +"@objectstack/plugin-webhooks": patch +--- + +chore(i18n): purge the dead sys_webhook_delivery translation block and guard against recurrence + +`sys_webhook_delivery` was removed when webhook delivery moved to +`@objectstack/service-messaging` (`sys_http_delivery`, ADR-0018 M3), but a full +translation block for it lingered in the four generated plugin-webhooks i18n +bundles (en/zh-CN/ja-JP/es-ES) — dead weight bound to an object that no longer +exists, and destined to be dropped silently (with any curated strings) on the +next `os i18n extract`. + +- Removed the stale `sys_webhook_delivery` block from all four locale bundles + (surgical; the `sys_webhook` block is untouched). +- Corrected three stale `sys_webhook_delivery` doc comments (platform-objects + `integration/index.ts` + `setup.app.ts`, plugin-webhooks `sys-webhook.object.ts`) + that still named it as a plugin-webhooks-owned object. +- Rolled out the platform-objects `bundle-ownership` test guard (#2834 ⑤ / + ADR-0029 D8) to the eight packages that own i18n bundles, so a stray object + block in a generated bundle now fails the build instead of dying silently. +- That guard immediately surfaced a live-object omission: `sys_capability` was + present in plugin-security's bundles with curated translations but had been + dropped from its extract config — re-added to the config so the strings are + preserved, rather than deleted. diff --git a/packages/platform-objects/src/apps/setup.app.ts b/packages/platform-objects/src/apps/setup.app.ts index 5614a326a9..4d0bba95f4 100644 --- a/packages/platform-objects/src/apps/setup.app.ts +++ b/packages/platform-objects/src/apps/setup.app.ts @@ -14,7 +14,7 @@ * alongside this app. * - Items owned by a capability plugin are contributed by that plugin — e.g. * `@objectstack/plugin-webhooks` fills `group_integrations` with its - * `sys_webhook` / `sys_webhook_delivery` entries (ADR-0029 K2.a). + * `sys_webhook` entry (ADR-0029 K2.a). * * The runtime merges all contributions into this app's `navigation` tree by * group id + priority on read, so the rendered Setup nav is identical to the diff --git a/packages/platform-objects/src/integration/index.ts b/packages/platform-objects/src/integration/index.ts index de3b69092b..eb948397f3 100644 --- a/packages/platform-objects/src/integration/index.ts +++ b/packages/platform-objects/src/integration/index.ts @@ -4,9 +4,9 @@ * platform-objects/integration — External Integration Platform Objects * * **Empty since ADR-0029 (K2.a).** `sys_webhook` moved to its owner, - * `@objectstack/plugin-webhooks` (alongside `sys_webhook_delivery`), so the - * plugin ships its data model and behavior as one unit. Import the schema - * from `@objectstack/plugin-webhooks/schema` instead. + * `@objectstack/plugin-webhooks`, so the plugin ships its data model and + * behavior as one unit. Import the schema from + * `@objectstack/plugin-webhooks/schema` instead. * * The subpath (`@objectstack/plugin-webhooks/integration`) is retained as an * empty barrel to avoid churning the package `exports` map / tsup entries diff --git a/packages/plugins/plugin-approvals/src/translations/bundle-ownership.test.ts b/packages/plugins/plugin-approvals/src/translations/bundle-ownership.test.ts new file mode 100644 index 0000000000..1ac5a6c089 --- /dev/null +++ b/packages/plugins/plugin-approvals/src/translations/bundle-ownership.test.ts @@ -0,0 +1,48 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Bundle-ownership guard (#2834 ⑤ / ADR-0029 D8): this package's generated +// object-translation bundles must carry ONLY objects its extract config +// (`scripts/i18n-extract.config.ts`) actually imports. When an object moves to +// another package, its translations move with it — a leftover copy here +// silently DIES on the next `os i18n extract` run, taking curated translations +// with it (the sys_audit_log incident). This test turns that silent loss into a +// red build: an object present in the bundle but not in the ownership list below +// means either (a) the extract config gained an object — add it here — or (b) a +// moved/removed object's keys were left behind — remove them from the bundles +// (or migrate them to the owning package), then keep this list in sync. +// +// NB: this package defines more approval objects (sys_approval_approver, +// sys_approval_token) that the extract config deliberately does NOT translate — +// they are absent from both the config and the bundles, so they are correctly +// out of scope here. + +import { describe, it, expect } from 'vitest'; +import { enObjects } from './en.objects.generated.js'; + +// Objects the extract config (scripts/i18n-extract.config.ts) imports — +// keep the two lists in sync when adding/moving objects. +const OWNED_OBJECTS = new Set([ + 'sys_approval_request', + 'sys_approval_action', + 'sys_approval_delegation', +]); + +describe('objects translation bundle ownership (ADR-0029 D8)', () => { + it('the en bundle contains no objects owned by other packages', () => { + const strays = Object.keys(enObjects).filter((o) => !OWNED_OBJECTS.has(o)); + expect( + strays, + `bundle carries objects this package's extract config does not own: ${strays.join(', ')} — ` + + 'their curated translations would be silently deleted on the next `os i18n extract`. ' + + 'Remove the dead block from the four locale bundles, or add the object to the extract config + this list.', + ).toEqual([]); + }); + + it('every owned object is present in the bundle (extract config regression)', () => { + const missing = [...OWNED_OBJECTS].filter((o) => !(o in enObjects)); + expect( + missing, + `objects the extract config should emit are missing from the bundle: ${missing.join(', ')} — was an import dropped from scripts/i18n-extract.config.ts?`, + ).toEqual([]); + }); +}); diff --git a/packages/plugins/plugin-audit/src/translations/bundle-ownership.test.ts b/packages/plugins/plugin-audit/src/translations/bundle-ownership.test.ts new file mode 100644 index 0000000000..c6fe8d33d5 --- /dev/null +++ b/packages/plugins/plugin-audit/src/translations/bundle-ownership.test.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Bundle-ownership guard (#2834 ⑤ / ADR-0029 D8): this package's generated +// object-translation bundles must carry ONLY objects its extract config +// (`scripts/i18n-extract.config.ts`) actually imports. When an object moves to +// another package, its translations move with it — a leftover copy here +// silently DIES on the next `os i18n extract` run, taking curated translations +// with it (the sys_audit_log incident). This test turns that silent loss into a +// red build: an object present in the bundle but not in the ownership list below +// means either (a) the extract config gained an object — add it here — or (b) a +// moved/removed object's keys were left behind — remove them from the bundles +// (or migrate them to the owning package), then keep this list in sync. + +import { describe, it, expect } from 'vitest'; +import { enObjects } from './en.objects.generated.js'; + +// Objects the extract config (scripts/i18n-extract.config.ts) imports — +// keep the two lists in sync when adding/moving objects. +const OWNED_OBJECTS = new Set([ + 'sys_audit_log', + 'sys_activity', + 'sys_comment', +]); + +describe('objects translation bundle ownership (ADR-0029 D8)', () => { + it('the en bundle contains no objects owned by other packages', () => { + const strays = Object.keys(enObjects).filter((o) => !OWNED_OBJECTS.has(o)); + expect( + strays, + `bundle carries objects this package's extract config does not own: ${strays.join(', ')} — ` + + 'their curated translations would be silently deleted on the next `os i18n extract`. ' + + 'Remove the dead block from the four locale bundles, or add the object to the extract config + this list.', + ).toEqual([]); + }); + + it('every owned object is present in the bundle (extract config regression)', () => { + const missing = [...OWNED_OBJECTS].filter((o) => !(o in enObjects)); + expect( + missing, + `objects the extract config should emit are missing from the bundle: ${missing.join(', ')} — was an import dropped from scripts/i18n-extract.config.ts?`, + ).toEqual([]); + }); +}); diff --git a/packages/plugins/plugin-security/scripts/i18n-extract.config.ts b/packages/plugins/plugin-security/scripts/i18n-extract.config.ts index fea4fc1835..55eb32f106 100644 --- a/packages/plugins/plugin-security/scripts/i18n-extract.config.ts +++ b/packages/plugins/plugin-security/scripts/i18n-extract.config.ts @@ -13,7 +13,11 @@ */ import { defineStack } from '@objectstack/spec'; -import { SysPosition, SysPermissionSet, SysUserPermissionSet, SysPositionPermissionSet } from '../src/objects/index.js'; +// SysCapability carries curated translations already present in the bundles; it +// must stay in this list so `os i18n extract` keeps emitting it (dropping it +// here silently deletes those strings on the next run — the sys_audit_log +// incident). Enforced by src/translations/bundle-ownership.test.ts. +import { SysPosition, SysCapability, SysPermissionSet, SysUserPermissionSet, SysPositionPermissionSet } from '../src/objects/index.js'; import { enObjects } from '../src/translations/en.objects.generated.js'; import { zhCNObjects } from '../src/translations/zh-CN.objects.generated.js'; import { jaJPObjects } from '../src/translations/ja-JP.objects.generated.js'; @@ -21,7 +25,7 @@ import { esESObjects } from '../src/translations/es-ES.objects.generated.js'; export default defineStack({ name: 'plugin-security-i18n-extract', - objects: [SysPosition, SysPermissionSet, SysUserPermissionSet, SysPositionPermissionSet] as any, + objects: [SysPosition, SysCapability, SysPermissionSet, SysUserPermissionSet, SysPositionPermissionSet] as any, translations: [ { en: { objects: enObjects } }, { 'zh-CN': { objects: zhCNObjects } }, diff --git a/packages/plugins/plugin-security/src/translations/bundle-ownership.test.ts b/packages/plugins/plugin-security/src/translations/bundle-ownership.test.ts new file mode 100644 index 0000000000..ec27c27830 --- /dev/null +++ b/packages/plugins/plugin-security/src/translations/bundle-ownership.test.ts @@ -0,0 +1,52 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Bundle-ownership guard (#2834 ⑤ / ADR-0029 D8): this package's generated +// object-translation bundles must carry ONLY objects its extract config +// (`scripts/i18n-extract.config.ts`) actually imports. When an object moves to +// another package, its translations move with it — a leftover copy here +// silently DIES on the next `os i18n extract` run, taking curated translations +// with it (the sys_audit_log incident). This test turns that silent loss into a +// red build: an object present in the bundle but not in the ownership list below +// means either (a) the extract config gained an object — add it here — or (b) a +// moved/removed object's keys were left behind — remove them from the bundles +// (or migrate them to the owning package), then keep this list in sync. +// +// NB: this package defines more objects (sys_user_position, +// sys_audience_binding_suggestion) that the extract config deliberately does NOT +// translate — absent from both the config and the bundles, correctly out of +// scope. `sys_capability` IS translated (curated strings live in the bundles), +// so it must stay in the extract config; this guard is what caught it being +// dropped from that list. + +import { describe, it, expect } from 'vitest'; +import { enObjects } from './en.objects.generated.js'; + +// Objects the extract config (scripts/i18n-extract.config.ts) imports — +// keep the two lists in sync when adding/moving objects. +const OWNED_OBJECTS = new Set([ + 'sys_position', + 'sys_capability', + 'sys_permission_set', + 'sys_user_permission_set', + 'sys_position_permission_set', +]); + +describe('objects translation bundle ownership (ADR-0029 D8)', () => { + it('the en bundle contains no objects owned by other packages', () => { + const strays = Object.keys(enObjects).filter((o) => !OWNED_OBJECTS.has(o)); + expect( + strays, + `bundle carries objects this package's extract config does not own: ${strays.join(', ')} — ` + + 'their curated translations would be silently deleted on the next `os i18n extract`. ' + + 'Remove the dead block from the four locale bundles, or add the object to the extract config + this list.', + ).toEqual([]); + }); + + it('every owned object is present in the bundle (extract config regression)', () => { + const missing = [...OWNED_OBJECTS].filter((o) => !(o in enObjects)); + expect( + missing, + `objects the extract config should emit are missing from the bundle: ${missing.join(', ')} — was an import dropped from scripts/i18n-extract.config.ts?`, + ).toEqual([]); + }); +}); diff --git a/packages/plugins/plugin-sharing/src/translations/bundle-ownership.test.ts b/packages/plugins/plugin-sharing/src/translations/bundle-ownership.test.ts new file mode 100644 index 0000000000..bc217a9a02 --- /dev/null +++ b/packages/plugins/plugin-sharing/src/translations/bundle-ownership.test.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Bundle-ownership guard (#2834 ⑤ / ADR-0029 D8): this package's generated +// object-translation bundles must carry ONLY objects its extract config +// (`scripts/i18n-extract.config.ts`) actually imports. When an object moves to +// another package, its translations move with it — a leftover copy here +// silently DIES on the next `os i18n extract` run, taking curated translations +// with it (the sys_audit_log incident). This test turns that silent loss into a +// red build: an object present in the bundle but not in the ownership list below +// means either (a) the extract config gained an object — add it here — or (b) a +// moved/removed object's keys were left behind — remove them from the bundles +// (or migrate them to the owning package), then keep this list in sync. + +import { describe, it, expect } from 'vitest'; +import { enObjects } from './en.objects.generated.js'; + +// Objects the extract config (scripts/i18n-extract.config.ts) imports — +// keep the two lists in sync when adding/moving objects. +const OWNED_OBJECTS = new Set([ + 'sys_record_share', + 'sys_sharing_rule', + 'sys_share_link', +]); + +describe('objects translation bundle ownership (ADR-0029 D8)', () => { + it('the en bundle contains no objects owned by other packages', () => { + const strays = Object.keys(enObjects).filter((o) => !OWNED_OBJECTS.has(o)); + expect( + strays, + `bundle carries objects this package's extract config does not own: ${strays.join(', ')} — ` + + 'their curated translations would be silently deleted on the next `os i18n extract`. ' + + 'Remove the dead block from the four locale bundles, or add the object to the extract config + this list.', + ).toEqual([]); + }); + + it('every owned object is present in the bundle (extract config regression)', () => { + const missing = [...OWNED_OBJECTS].filter((o) => !(o in enObjects)); + expect( + missing, + `objects the extract config should emit are missing from the bundle: ${missing.join(', ')} — was an import dropped from scripts/i18n-extract.config.ts?`, + ).toEqual([]); + }); +}); diff --git a/packages/plugins/plugin-webhooks/src/sys-webhook.object.ts b/packages/plugins/plugin-webhooks/src/sys-webhook.object.ts index 5ac357c33e..1c3868d9df 100644 --- a/packages/plugins/plugin-webhooks/src/sys-webhook.object.ts +++ b/packages/plugins/plugin-webhooks/src/sys-webhook.object.ts @@ -25,11 +25,10 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; * (ADR-0018 M3 — `sys_http_delivery`, drained by the messaging dispatcher). * * Ownership (ADR-0029 K2.a): this object is **owned by - * `@objectstack/plugin-webhooks`** — the plugin that consumes these rows — - * alongside its sibling `sys_webhook_delivery`. It used to live in the - * `@objectstack/platform-objects` monolith and be imported here; the - * definition now lives with its owner so the plugin ships both data and - * behavior as one unit. + * `@objectstack/plugin-webhooks`** — the plugin that consumes these rows. It + * used to live in the `@objectstack/platform-objects` monolith and be imported + * here; the definition now lives with its owner so the plugin ships both data + * and behavior as one unit. * * Platform-wide on purpose: every project (standalone, single-tenant, * cloud) can integrate with external systems (Slack, Stripe, internal diff --git a/packages/plugins/plugin-webhooks/src/translations/bundle-ownership.test.ts b/packages/plugins/plugin-webhooks/src/translations/bundle-ownership.test.ts new file mode 100644 index 0000000000..d5e266631b --- /dev/null +++ b/packages/plugins/plugin-webhooks/src/translations/bundle-ownership.test.ts @@ -0,0 +1,41 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Bundle-ownership guard (#2834 ⑤ / ADR-0029 D8): this package's generated +// object-translation bundles must carry ONLY objects its extract config +// (`scripts/i18n-extract.config.ts`) actually imports. When an object moves to +// another package, its translations move with it — a leftover copy here +// silently DIES on the next `os i18n extract` run, taking curated translations +// with it (the sys_audit_log incident). This test turns that silent loss into a +// red build: an object present in the bundle but not in the ownership list below +// means either (a) the extract config gained an object — add it here — or (b) a +// moved/removed object's keys were left behind — remove them from the bundles +// (or migrate them to the owning package), then keep this list in sync. + +import { describe, it, expect } from 'vitest'; +import { enObjects } from './en.objects.generated.js'; + +// Objects the extract config (scripts/i18n-extract.config.ts) imports — +// keep the two lists in sync when adding/moving objects. +const OWNED_OBJECTS = new Set([ + 'sys_webhook', +]); + +describe('objects translation bundle ownership (ADR-0029 D8)', () => { + it('the en bundle contains no objects owned by other packages', () => { + const strays = Object.keys(enObjects).filter((o) => !OWNED_OBJECTS.has(o)); + expect( + strays, + `bundle carries objects this package's extract config does not own: ${strays.join(', ')} — ` + + 'their curated translations would be silently deleted on the next `os i18n extract`. ' + + 'Remove the dead block from the four locale bundles, or add the object to the extract config + this list.', + ).toEqual([]); + }); + + it('every owned object is present in the bundle (extract config regression)', () => { + const missing = [...OWNED_OBJECTS].filter((o) => !(o in enObjects)); + expect( + missing, + `objects the extract config should emit are missing from the bundle: ${missing.join(', ')} — was an import dropped from scripts/i18n-extract.config.ts?`, + ).toEqual([]); + }); +}); diff --git a/packages/plugins/plugin-webhooks/src/translations/en.objects.generated.ts b/packages/plugins/plugin-webhooks/src/translations/en.objects.generated.ts index 6152db492d..425895ecfc 100644 --- a/packages/plugins/plugin-webhooks/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-webhooks/src/translations/en.objects.generated.ts @@ -72,116 +72,5 @@ export const enObjects: NonNullable = { label: "All" } } - }, - sys_webhook_delivery: { - label: "Webhook Delivery", - pluralLabel: "Webhook Deliveries", - description: "Durable outbox row for one webhook attempt. Managed by @objectstack/plugin-webhooks; do not write directly.", - fields: { - id: { - label: "Delivery ID", - help: "UUID — also doubles as the receiver-side idempotency key" - }, - webhook_id: { - label: "Webhook ID", - help: "FK to sys_webhook.id (loosely coupled — denormalised URL/secret on row)" - }, - event_id: { - label: "Event ID", - help: "Source event id; UNIQUE(event_id, webhook_id) for dedup" - }, - event_type: { - label: "Event Type", - help: "e.g. data.record.created" - }, - url: { - label: "Target URL", - help: "Snapshotted at enqueue so config edits do not rewrite live rows" - }, - method: { - label: "Method" - }, - headers_json: { - label: "Headers JSON" - }, - secret: { - label: "HMAC Secret" - }, - timeout_ms: { - label: "Timeout (ms)" - }, - payload_json: { - label: "Payload JSON" - }, - partition_key: { - label: "Partition", - help: "hash(webhook_id) mod partitionCount — precomputed for cheap WHERE" - }, - status: { - label: "Status", - help: "pending | in_flight | success | failed | dead" - }, - attempts: { - label: "Attempts", - help: "Number of POST attempts made so far" - }, - claimed_by: { - label: "Claimed By" - }, - claimed_at: { - label: "Claimed At (ms)" - }, - next_retry_at: { - label: "Next Retry At (ms)" - }, - last_attempted_at: { - label: "Last Attempted At (ms)" - }, - response_code: { - label: "HTTP Status" - }, - response_body: { - label: "Response Body (capped)" - }, - error: { - label: "Error" - }, - created_at: { - label: "Created At (ms)" - }, - updated_at: { - label: "Updated At (ms)" - } - }, - _views: { - recent: { - label: "Recent" - }, - failures: { - label: "Failures" - }, - in_flight: { - label: "In Flight" - }, - pending: { - label: "Pending" - }, - by_status: { - label: "By Status" - }, - by_webhook: { - label: "By Webhook" - }, - all_deliveries: { - label: "All" - } - }, - _actions: { - redeliver: { - label: "Redeliver", - confirmText: "Replay this delivery? The receiver will get the original payload again — they must be idempotent on the X-Objectstack-Delivery header.", - successMessage: "Queued for redelivery" - } - } } }; diff --git a/packages/plugins/plugin-webhooks/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-webhooks/src/translations/es-ES.objects.generated.ts index e3649f7345..99a5576520 100644 --- a/packages/plugins/plugin-webhooks/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-webhooks/src/translations/es-ES.objects.generated.ts @@ -72,116 +72,5 @@ export const esESObjects: NonNullable = { label: "Todos" } } - }, - sys_webhook_delivery: { - label: "Webhook Delivery", - pluralLabel: "Webhook Deliveries", - description: "Durable outbox row for one webhook attempt. Managed by @objectstack/plugin-webhooks; do not write directly.", - fields: { - id: { - label: "Delivery ID", - help: "UUID — also doubles as the receiver-side idempotency key" - }, - webhook_id: { - label: "Webhook ID", - help: "FK to sys_webhook.id (loosely coupled — denormalised URL/secret on row)" - }, - event_id: { - label: "Event ID", - help: "Source event id; UNIQUE(event_id, webhook_id) for dedup" - }, - event_type: { - label: "Event Type", - help: "e.g. data.record.created" - }, - url: { - label: "Target URL", - help: "Snapshotted at enqueue so config edits do not rewrite live rows" - }, - method: { - label: "Method" - }, - headers_json: { - label: "Headers JSON" - }, - secret: { - label: "HMAC Secret" - }, - timeout_ms: { - label: "Timeout (ms)" - }, - payload_json: { - label: "Payload JSON" - }, - partition_key: { - label: "Partition", - help: "hash(webhook_id) mod partitionCount — precomputed for cheap WHERE" - }, - status: { - label: "Status", - help: "pending | in_flight | success | failed | dead" - }, - attempts: { - label: "Attempts", - help: "Number of POST attempts made so far" - }, - claimed_by: { - label: "Claimed By" - }, - claimed_at: { - label: "Claimed At (ms)" - }, - next_retry_at: { - label: "Next Retry At (ms)" - }, - last_attempted_at: { - label: "Last Attempted At (ms)" - }, - response_code: { - label: "HTTP Status" - }, - response_body: { - label: "Response Body (capped)" - }, - error: { - label: "Error" - }, - created_at: { - label: "Created At (ms)" - }, - updated_at: { - label: "Updated At (ms)" - } - }, - _views: { - recent: { - label: "Recent" - }, - failures: { - label: "Failures" - }, - in_flight: { - label: "In Flight" - }, - pending: { - label: "Pending" - }, - by_status: { - label: "By Status" - }, - by_webhook: { - label: "By Webhook" - }, - all_deliveries: { - label: "All" - } - }, - _actions: { - redeliver: { - label: "Redeliver", - confirmText: "Replay this delivery? The receiver will get the original payload again — they must be idempotent on the X-Objectstack-Delivery header.", - successMessage: "Queued for redelivery" - } - } } }; diff --git a/packages/plugins/plugin-webhooks/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-webhooks/src/translations/ja-JP.objects.generated.ts index e4ab1ec05e..4e684b7d28 100644 --- a/packages/plugins/plugin-webhooks/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-webhooks/src/translations/ja-JP.objects.generated.ts @@ -72,116 +72,5 @@ export const jaJPObjects: NonNullable = { label: "すべて" } } - }, - sys_webhook_delivery: { - label: "Webhook Delivery", - pluralLabel: "Webhook Deliveries", - description: "Durable outbox row for one webhook attempt. Managed by @objectstack/plugin-webhooks; do not write directly.", - fields: { - id: { - label: "Delivery ID", - help: "UUID — also doubles as the receiver-side idempotency key" - }, - webhook_id: { - label: "Webhook ID", - help: "FK to sys_webhook.id (loosely coupled — denormalised URL/secret on row)" - }, - event_id: { - label: "Event ID", - help: "Source event id; UNIQUE(event_id, webhook_id) for dedup" - }, - event_type: { - label: "Event Type", - help: "e.g. data.record.created" - }, - url: { - label: "Target URL", - help: "Snapshotted at enqueue so config edits do not rewrite live rows" - }, - method: { - label: "Method" - }, - headers_json: { - label: "Headers JSON" - }, - secret: { - label: "HMAC Secret" - }, - timeout_ms: { - label: "Timeout (ms)" - }, - payload_json: { - label: "Payload JSON" - }, - partition_key: { - label: "Partition", - help: "hash(webhook_id) mod partitionCount — precomputed for cheap WHERE" - }, - status: { - label: "Status", - help: "pending | in_flight | success | failed | dead" - }, - attempts: { - label: "Attempts", - help: "Number of POST attempts made so far" - }, - claimed_by: { - label: "Claimed By" - }, - claimed_at: { - label: "Claimed At (ms)" - }, - next_retry_at: { - label: "Next Retry At (ms)" - }, - last_attempted_at: { - label: "Last Attempted At (ms)" - }, - response_code: { - label: "HTTP Status" - }, - response_body: { - label: "Response Body (capped)" - }, - error: { - label: "Error" - }, - created_at: { - label: "Created At (ms)" - }, - updated_at: { - label: "Updated At (ms)" - } - }, - _views: { - recent: { - label: "Recent" - }, - failures: { - label: "Failures" - }, - in_flight: { - label: "In Flight" - }, - pending: { - label: "Pending" - }, - by_status: { - label: "By Status" - }, - by_webhook: { - label: "By Webhook" - }, - all_deliveries: { - label: "All" - } - }, - _actions: { - redeliver: { - label: "Redeliver", - confirmText: "Replay this delivery? The receiver will get the original payload again — they must be idempotent on the X-Objectstack-Delivery header.", - successMessage: "Queued for redelivery" - } - } } }; diff --git a/packages/plugins/plugin-webhooks/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-webhooks/src/translations/zh-CN.objects.generated.ts index 5e4c489aa8..a3f4eb63b3 100644 --- a/packages/plugins/plugin-webhooks/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-webhooks/src/translations/zh-CN.objects.generated.ts @@ -72,116 +72,5 @@ export const zhCNObjects: NonNullable = { label: "全部" } } - }, - sys_webhook_delivery: { - label: "Webhook Delivery", - pluralLabel: "Webhook Deliveries", - description: "Durable outbox row for one webhook attempt. Managed by @objectstack/plugin-webhooks; do not write directly.", - fields: { - id: { - label: "Delivery ID", - help: "UUID — also doubles as the receiver-side idempotency key" - }, - webhook_id: { - label: "Webhook ID", - help: "FK to sys_webhook.id (loosely coupled — denormalised URL/secret on row)" - }, - event_id: { - label: "Event ID", - help: "Source event id; UNIQUE(event_id, webhook_id) for dedup" - }, - event_type: { - label: "Event Type", - help: "e.g. data.record.created" - }, - url: { - label: "Target URL", - help: "Snapshotted at enqueue so config edits do not rewrite live rows" - }, - method: { - label: "Method" - }, - headers_json: { - label: "Headers JSON" - }, - secret: { - label: "HMAC Secret" - }, - timeout_ms: { - label: "Timeout (ms)" - }, - payload_json: { - label: "Payload JSON" - }, - partition_key: { - label: "Partition", - help: "hash(webhook_id) mod partitionCount — precomputed for cheap WHERE" - }, - status: { - label: "Status", - help: "pending | in_flight | success | failed | dead" - }, - attempts: { - label: "Attempts", - help: "Number of POST attempts made so far" - }, - claimed_by: { - label: "Claimed By" - }, - claimed_at: { - label: "Claimed At (ms)" - }, - next_retry_at: { - label: "Next Retry At (ms)" - }, - last_attempted_at: { - label: "Last Attempted At (ms)" - }, - response_code: { - label: "HTTP Status" - }, - response_body: { - label: "Response Body (capped)" - }, - error: { - label: "Error" - }, - created_at: { - label: "Created At (ms)" - }, - updated_at: { - label: "Updated At (ms)" - } - }, - _views: { - recent: { - label: "Recent" - }, - failures: { - label: "Failures" - }, - in_flight: { - label: "In Flight" - }, - pending: { - label: "Pending" - }, - by_status: { - label: "By Status" - }, - by_webhook: { - label: "By Webhook" - }, - all_deliveries: { - label: "All" - } - }, - _actions: { - redeliver: { - label: "Redeliver", - confirmText: "Replay this delivery? The receiver will get the original payload again — they must be idempotent on the X-Objectstack-Delivery header.", - successMessage: "Queued for redelivery" - } - } } }; diff --git a/packages/services/service-messaging/src/translations/bundle-ownership.test.ts b/packages/services/service-messaging/src/translations/bundle-ownership.test.ts new file mode 100644 index 0000000000..a90156fec8 --- /dev/null +++ b/packages/services/service-messaging/src/translations/bundle-ownership.test.ts @@ -0,0 +1,47 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Bundle-ownership guard (#2834 ⑤ / ADR-0029 D8): this package's generated +// object-translation bundles must carry ONLY objects its extract config +// (`scripts/i18n-extract.config.ts`) actually imports. When an object moves to +// another package, its translations move with it — a leftover copy here +// silently DIES on the next `os i18n extract` run, taking curated translations +// with it (the sys_audit_log incident). This test turns that silent loss into a +// red build: an object present in the bundle but not in the ownership list below +// means either (a) the extract config gained an object — add it here — or (b) a +// moved/removed object's keys were left behind — remove them from the bundles +// (or migrate them to the owning package), then keep this list in sync. + +import { describe, it, expect } from 'vitest'; +import { enObjects } from './en.objects.generated.js'; + +// Objects the extract config (scripts/i18n-extract.config.ts) imports — +// keep the two lists in sync when adding/moving objects. +const OWNED_OBJECTS = new Set([ + 'sys_inbox_message', + 'sys_notification_receipt', + 'sys_notification_delivery', + 'sys_notification_preference', + 'sys_notification_subscription', + 'sys_notification_template', + 'sys_http_delivery', +]); + +describe('objects translation bundle ownership (ADR-0029 D8)', () => { + it('the en bundle contains no objects owned by other packages', () => { + const strays = Object.keys(enObjects).filter((o) => !OWNED_OBJECTS.has(o)); + expect( + strays, + `bundle carries objects this package's extract config does not own: ${strays.join(', ')} — ` + + 'their curated translations would be silently deleted on the next `os i18n extract`. ' + + 'Remove the dead block from the four locale bundles, or add the object to the extract config + this list.', + ).toEqual([]); + }); + + it('every owned object is present in the bundle (extract config regression)', () => { + const missing = [...OWNED_OBJECTS].filter((o) => !(o in enObjects)); + expect( + missing, + `objects the extract config should emit are missing from the bundle: ${missing.join(', ')} — was an import dropped from scripts/i18n-extract.config.ts?`, + ).toEqual([]); + }); +}); diff --git a/packages/services/service-realtime/src/translations/bundle-ownership.test.ts b/packages/services/service-realtime/src/translations/bundle-ownership.test.ts new file mode 100644 index 0000000000..6d9591ca76 --- /dev/null +++ b/packages/services/service-realtime/src/translations/bundle-ownership.test.ts @@ -0,0 +1,41 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Bundle-ownership guard (#2834 ⑤ / ADR-0029 D8): this package's generated +// object-translation bundles must carry ONLY objects its extract config +// (`scripts/i18n-extract.config.ts`) actually imports. When an object moves to +// another package, its translations move with it — a leftover copy here +// silently DIES on the next `os i18n extract` run, taking curated translations +// with it (the sys_audit_log incident). This test turns that silent loss into a +// red build: an object present in the bundle but not in the ownership list below +// means either (a) the extract config gained an object — add it here — or (b) a +// moved/removed object's keys were left behind — remove them from the bundles +// (or migrate them to the owning package), then keep this list in sync. + +import { describe, it, expect } from 'vitest'; +import { enObjects } from './en.objects.generated.js'; + +// Objects the extract config (scripts/i18n-extract.config.ts) imports — +// keep the two lists in sync when adding/moving objects. +const OWNED_OBJECTS = new Set([ + 'sys_presence', +]); + +describe('objects translation bundle ownership (ADR-0029 D8)', () => { + it('the en bundle contains no objects owned by other packages', () => { + const strays = Object.keys(enObjects).filter((o) => !OWNED_OBJECTS.has(o)); + expect( + strays, + `bundle carries objects this package's extract config does not own: ${strays.join(', ')} — ` + + 'their curated translations would be silently deleted on the next `os i18n extract`. ' + + 'Remove the dead block from the four locale bundles, or add the object to the extract config + this list.', + ).toEqual([]); + }); + + it('every owned object is present in the bundle (extract config regression)', () => { + const missing = [...OWNED_OBJECTS].filter((o) => !(o in enObjects)); + expect( + missing, + `objects the extract config should emit are missing from the bundle: ${missing.join(', ')} — was an import dropped from scripts/i18n-extract.config.ts?`, + ).toEqual([]); + }); +}); diff --git a/packages/services/service-storage/src/translations/bundle-ownership.test.ts b/packages/services/service-storage/src/translations/bundle-ownership.test.ts new file mode 100644 index 0000000000..97b58c2cbc --- /dev/null +++ b/packages/services/service-storage/src/translations/bundle-ownership.test.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Bundle-ownership guard (#2834 ⑤ / ADR-0029 D8): this package's generated +// object-translation bundles must carry ONLY objects its extract config +// (`scripts/i18n-extract.config.ts`) actually imports. When an object moves to +// another package, its translations move with it — a leftover copy here +// silently DIES on the next `os i18n extract` run, taking curated translations +// with it (the sys_audit_log incident). This test turns that silent loss into a +// red build: an object present in the bundle but not in the ownership list below +// means either (a) the extract config gained an object — add it here — or (b) a +// moved/removed object's keys were left behind — remove them from the bundles +// (or migrate them to the owning package), then keep this list in sync. + +import { describe, it, expect } from 'vitest'; +import { enObjects } from './en.objects.generated.js'; + +// Objects the extract config (scripts/i18n-extract.config.ts) imports — +// keep the two lists in sync when adding/moving objects. +const OWNED_OBJECTS = new Set([ + 'sys_file', + 'sys_upload_session', +]); + +describe('objects translation bundle ownership (ADR-0029 D8)', () => { + it('the en bundle contains no objects owned by other packages', () => { + const strays = Object.keys(enObjects).filter((o) => !OWNED_OBJECTS.has(o)); + expect( + strays, + `bundle carries objects this package's extract config does not own: ${strays.join(', ')} — ` + + 'their curated translations would be silently deleted on the next `os i18n extract`. ' + + 'Remove the dead block from the four locale bundles, or add the object to the extract config + this list.', + ).toEqual([]); + }); + + it('every owned object is present in the bundle (extract config regression)', () => { + const missing = [...OWNED_OBJECTS].filter((o) => !(o in enObjects)); + expect( + missing, + `objects the extract config should emit are missing from the bundle: ${missing.join(', ')} — was an import dropped from scripts/i18n-extract.config.ts?`, + ).toEqual([]); + }); +});