Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion packages/platform-objects/src/apps/setup.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/platform-objects/src/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@
*/

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';
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 } },
Expand Down
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
9 changes: 4 additions & 5 deletions packages/plugins/plugin-webhooks/src/sys-webhook.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
Loading