diff --git a/.changeset/23802-setup-copy.md b/.changeset/23802-setup-copy.md new file mode 100644 index 00000000000..bf141cf265e --- /dev/null +++ b/.changeset/23802-setup-copy.md @@ -0,0 +1,18 @@ +--- +'@objectstack/platform-objects': patch +'@objectstack/metadata-core': patch +'@objectstack/service-messaging': patch +--- + +Clause-②: no + +Setup pages describe their records in plain admin sentences. Business Units now reads "Your +company structure — divisions, departments, regions and offices — used to scope data and +reports.", Notification Events "Every notification the platform sent, one row per event.", +Notification Preferences "Which topics and channels each user receives; admins set the +defaults.", Notification Templates "The subject and body each notification topic sends, per +channel and language." and HTTP Deliveries "Outgoing HTTP calls made by webhooks and flows, one +row per call, with its delivery status." Field help on Users, Organizations, Invitations and SSO +Providers, and the metadata-storage and notification-delivery object descriptions, no longer +carry design-record numbers; those references stay in the code comments. The zh-CN, ja-JP and +es-ES translations follow. Wording only: no field, permission or behaviour changes. diff --git a/packages/metadata-core/src/objects/sys-metadata-audit.object.ts b/packages/metadata-core/src/objects/sys-metadata-audit.object.ts index 878bcd06f58..052b207b5f4 100644 --- a/packages/metadata-core/src/objects/sys-metadata-audit.object.ts +++ b/packages/metadata-core/src/objects/sys-metadata-audit.object.ts @@ -45,7 +45,8 @@ export const SysMetadataAuditObject = ObjectSchema.create({ retention: { maxAge: '365d' }, archive: { after: '365d', to: 'archive' }, }, - description: 'Append-only audit trail of metadata write decisions (ADR-0010).', + // ADR-0010 — the reference lives in the docblock above, not in the copy. + description: 'Append-only audit trail of metadata write decisions.', fields: { /** Primary Key (UUID) */ diff --git a/packages/metadata-core/src/objects/sys-metadata-commit.object.ts b/packages/metadata-core/src/objects/sys-metadata-commit.object.ts index 2fa45e7fc85..dbca34196fd 100644 --- a/packages/metadata-core/src/objects/sys-metadata-commit.object.ts +++ b/packages/metadata-core/src/objects/sys-metadata-commit.object.ts @@ -28,7 +28,8 @@ export const SysMetadataCommitObject = ObjectSchema.create({ icon: 'git-commit', isSystem: true, managedBy: 'engine-owned', - description: 'Package-scoped commit log grouping a turn’s metadata changes (ADR-0067).', + // ADR-0067 — the reference lives in the docblock above, not in the copy. + description: 'Package-scoped commit log grouping a turn’s metadata changes.', fields: { /** diff --git a/packages/metadata-core/src/objects/sys-metadata.object.ts b/packages/metadata-core/src/objects/sys-metadata.object.ts index 8fcb75b41a5..81f9d8d8bc0 100644 --- a/packages/metadata-core/src/objects/sys-metadata.object.ts +++ b/packages/metadata-core/src/objects/sys-metadata.object.ts @@ -158,7 +158,8 @@ export const SysMetadataObject = ObjectSchema.create({ environment_id: Field.lookup('sys_environment', { label: 'Environment (deprecated)', required: false, - description: 'DEPRECATED. Overlays are environment rows (ADR-0131 D6); no tenant column.', + // ADR-0131 D6 — overlays are environment rows. + description: 'DEPRECATED. Overlays are environment rows; no tenant column.', }), /** Version number for optimistic concurrency */ diff --git a/packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts b/packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts new file mode 100644 index 00000000000..78f18880f31 --- /dev/null +++ b/packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts @@ -0,0 +1,185 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Setup copy carries no design-record vocabulary. + * + * An administrator reads three kinds of string this package ships: the + * sentence under a Setup page title (the object's `description`), a field's + * help tooltip (the field's `description`, extracted as `help`), and the Setup + * left nav (`SETUP_APP` plus `SETUP_NAV_CONTRIBUTIONS`). The generated bundles + * carry the same strings into every locale. An ADR number, a decision id + * ("D2"), a review-finding id ("finding F2"), an API call ("emit()") or a + * runtime internal ("kernel") means nothing to that reader: the reference + * belongs in the docblock or comment next to the string, never in the string. + * + * Population, all of it read from this checkout's source (no build artifact + * except `@objectstack/metadata-core`, a registered unaliased pair in + * `scripts/check-test-source-alias.mjs`): + * + * 1. every object schema this package exports, plus every one + * `@objectstack/metadata-core` exports (`sys_metadata_commit` included, + * which no bundle carries) — each `label`, `pluralLabel` and + * `description` anywhere in the definition; + * 2. every leaf of the four committed `objects` bundles; + * 3. the type-level `label` / `description` of the four `metadataForms` + * bundles — the sentence a Setup metadata page (Positions, Capabilities, + * Permission Sets) shows under its title. Section descriptions and field + * `helpText` of those forms are the spec's form copy and are not judged + * here; + * 4. `SETUP_APP` and this package's `SETUP_NAV_CONTRIBUTIONS`, and the + * hand-authored `apps.setup` subtree of each locale. + * + * Nav entries other packages contribute, and the service-messaging objects, + * are outside this package's import graph; `service-messaging` pins its own + * objects in `src/objects/admin-copy-vocabulary.test.ts`. + */ + +import { describe, expect, it } from 'vitest'; +import * as MetadataCore from '@objectstack/metadata-core'; +import * as PlatformObjects from '../index.js'; +import { SETUP_APP } from './setup.app.js'; +import { SETUP_NAV_CONTRIBUTIONS } from './setup-nav.contributions.js'; +import { en } from './translations/en.js'; +import { zhCN } from './translations/zh-CN.js'; +import { jaJP } from './translations/ja-JP.js'; +import { esES } from './translations/es-ES.js'; +import { enObjects } from './translations/en.objects.generated.js'; +import { zhCNObjects } from './translations/zh-CN.objects.generated.js'; +import { jaJPObjects } from './translations/ja-JP.objects.generated.js'; +import { esESObjects } from './translations/es-ES.objects.generated.js'; +import { enMetadataForms } from './translations/en.metadata-forms.generated.js'; +import { zhCNMetadataForms } from './translations/zh-CN.metadata-forms.generated.js'; +import { jaJPMetadataForms } from './translations/ja-JP.metadata-forms.generated.js'; +import { esESMetadataForms } from './translations/es-ES.metadata-forms.generated.js'; + +/** The design-record vocabulary an admin-facing string must not carry. */ +const DESIGN_RECORD_VOCABULARY: ReadonlyArray = [ + ['an ADR number', /ADR-\d/], + ['a decision id', /\bD\d+\b/], + ['a review-finding id', /finding F/], + ['an API call', /\bemit\(\)/], + ['a runtime internal', /\bkernel\b/i], +]; + +function vocabularyIn(text: string): string[] { + return DESIGN_RECORD_VOCABULARY.filter(([, re]) => re.test(text)).map(([name]) => name); +} + +const COPY_KEYS = new Set(['label', 'pluralLabel', 'description']); + +/** Every `label` / `pluralLabel` / `description` string under `node`, with its path. */ +function copyStrings(node: unknown, path: string, out: Array<[string, string]>): void { + if (Array.isArray(node)) { + node.forEach((v, i) => copyStrings(v, `${path}[${i}]`, out)); + return; + } + if (!node || typeof node !== 'object') return; + for (const [k, v] of Object.entries(node)) { + const at = path ? `${path}.${k}` : k; + if (typeof v === 'string') { + if (COPY_KEYS.has(k)) out.push([at, v]); + } else { + copyStrings(v, at, out); + } + } +} + +/** Every string leaf under `node`, with its path. */ +function leafStrings(node: unknown, path: string, out: Array<[string, string]>): void { + if (typeof node === 'string') { + out.push([path, node]); + return; + } + if (!node || typeof node !== 'object') return; + for (const [k, v] of Object.entries(node)) leafStrings(v, path ? `${path}.${k}` : k, out); +} + +function offenders(strings: Array<[string, string]>): string[] { + return strings + .map(([at, text]) => [at, text, vocabularyIn(text)] as const) + .filter(([, , hits]) => hits.length > 0) + .map(([at, text, hits]) => `${at} (${hits.join(', ')}): ${text}`); +} + +type ObjectLike = { name: string; fields: Record }; +function isObjectSchema(v: unknown): v is ObjectLike { + return !!v && typeof v === 'object' + && typeof (v as ObjectLike).name === 'string' + && !!(v as ObjectLike).fields && typeof (v as ObjectLike).fields === 'object'; +} + +function objectSchemas(...modules: Array>): Map { + const byName = new Map(); + for (const mod of modules) { + for (const v of Object.values(mod)) if (isObjectSchema(v) && !byName.has(v.name)) byName.set(v.name, v); + } + return byName; +} + +const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES } as const; +const OBJECT_BUNDLES = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects } as const; +const FORM_BUNDLES = { + en: enMetadataForms, 'zh-CN': zhCNMetadataForms, 'ja-JP': jaJPMetadataForms, 'es-ES': esESMetadataForms, +} as const; + +describe('Setup copy carries no design-record vocabulary', () => { + it('the matcher recognises each kind of vocabulary it exists to keep out', () => { + // A matcher that matches nothing passes every assertion below; each kind + // must be seen on a sentence of the shape it was written for. + expect(vocabularyIn('Business Unit tree (ADR-0057).')).toEqual(['an ADR number']); + expect(vocabularyIn('Placement intent, decision D12.')).toEqual(['a decision id']); + expect(vocabularyIn('Bypass rule (finding F2).')).toEqual(['a review-finding id']); + expect(vocabularyIn('One row per emit().')).toEqual(['an API call']); + expect(vocabularyIn('The Kernel API is additive.')).toEqual(['a runtime internal']); + expect(vocabularyIn('Your company structure, used to scope data and reports.')).toEqual([]); + }); + + it('object, field and view copy of every object this package and metadata-core export', () => { + const objects = objectSchemas( + PlatformObjects as unknown as Record, + MetadataCore as unknown as Record, + ); + // The population is not allowed to shrink to nothing: the objects whose + // copy this suite was written against must still be in it. + for (const name of [ + 'sys_user', 'sys_business_unit', 'sys_notification', 'sys_sso_provider', + 'sys_metadata', 'sys_metadata_audit', 'sys_metadata_commit', 'sys_metadata_activation', + ]) { + expect(objects.has(name), `${name} is in the population`).toBe(true); + } + expect(objects.size).toBeGreaterThanOrEqual(50); + + const strings: Array<[string, string]> = []; + for (const [name, def] of objects) copyStrings(def, name, strings); + expect(offenders(strings)).toEqual([]); + }); + + it.each(Object.keys(OBJECT_BUNDLES))('every leaf of the %s objects bundle', (locale) => { + const strings: Array<[string, string]> = []; + leafStrings(OBJECT_BUNDLES[locale as keyof typeof OBJECT_BUNDLES], `objects`, strings); + expect(strings.length).toBeGreaterThan(1000); + expect(offenders(strings)).toEqual([]); + }); + + it.each(Object.keys(FORM_BUNDLES))('the type-level label and description of the %s metadataForms bundle', (locale) => { + const forms = FORM_BUNDLES[locale as keyof typeof FORM_BUNDLES] as Record; + const strings: Array<[string, string]> = []; + for (const [type, form] of Object.entries(forms)) { + if (typeof form.label === 'string') strings.push([`metadataForms.${type}.label`, form.label]); + if (typeof form.description === 'string') strings.push([`metadataForms.${type}.description`, form.description]); + } + expect(forms.capability?.description, 'the Capability page sentence is in the population').toBeTypeOf('string'); + expect(offenders(strings)).toEqual([]); + }); + + it('the Setup app, its nav contributions, and every locale\'s apps.setup translations', () => { + const strings: Array<[string, string]> = []; + copyStrings(SETUP_APP, 'SETUP_APP', strings); + copyStrings(SETUP_NAV_CONTRIBUTIONS, 'SETUP_NAV_CONTRIBUTIONS', strings); + for (const [locale, data] of Object.entries(LOCALES)) { + leafStrings(data.apps?.setup, `${locale}.apps.setup`, strings); + } + expect(strings.some(([at]) => at.endsWith('nav_business_units.label'))).toBe(true); + expect(offenders(strings)).toEqual([]); + }); +}); diff --git a/packages/platform-objects/src/apps/translations/en.objects.generated.ts b/packages/platform-objects/src/apps/translations/en.objects.generated.ts index e9cb7ae48db..4e0314594c7 100644 --- a/packages/platform-objects/src/apps/translations/en.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.objects.generated.ts @@ -35,7 +35,7 @@ export const enObjects: NonNullable = { }, role: { label: "Platform Role", - help: "Legacy better-auth role scalar (admin, user, …). ObjectStack no longer writes it (ADR-0068 D2). To grant platform-admin standing, list the user's verified email in `OS_PLATFORM_OWNER_EMAIL`; under the `single` tenancy posture an unscoped `admin_full_access` assignment in `sys_user_permission_set` also confers it." + help: "Legacy better-auth role scalar (admin, user, …). ObjectStack no longer writes it. To grant platform-admin standing, list the user's verified email in `OS_PLATFORM_OWNER_EMAIL`; under the `single` tenancy posture an unscoped `admin_full_access` assignment in `sys_user_permission_set` also confers it." }, banned: { label: "Banned", @@ -97,11 +97,11 @@ export const enObjects: NonNullable = { }, manager_id: { label: "Manager", - help: "This user's direct manager. Forms the reporting chain the `own_and_reports` hierarchy scope walks (ADR-0057 / @objectstack/security-enterprise)." + help: "This user's direct manager. Forms the reporting chain the `own_and_reports` hierarchy scope walks." }, primary_business_unit_id: { label: "Primary Business Unit", - help: "The user's primary business unit — a denormalised projection of sys_business_unit_member.is_primary, maintained by plugin-sharing (ADR-0057 addendum D12). Lets a user-lookup filter candidates by business unit without traversing the membership junction. Do not edit directly; set it via business-unit membership." + help: "The user's primary business unit — a denormalised projection of sys_business_unit_member.is_primary, maintained by plugin-sharing. Lets a user-lookup filter candidates by business unit without traversing the membership junction. Do not edit directly; set it via business-unit membership." }, source: { label: "Identity Source", @@ -537,7 +537,7 @@ export const enObjects: NonNullable = { }, parent_organization_id: { label: "Parent Organization", - help: "Reporting/grouping parent. Grants NOTHING — visibility across organizations comes from membership, never from this reference (ADR-0105 D6)." + help: "Reporting/grouping parent. Grants NOTHING — visibility across organizations comes from membership, never from this reference." }, sort_order: { label: "Sort Order", @@ -710,11 +710,11 @@ export const enObjects: NonNullable = { }, business_unit_id: { label: "Placement Business Unit", - help: "Business unit the invitee is placed under on acceptance (ADR-0105 D8). Must lie inside the issuer's delegated subtree." + help: "Business unit the invitee is placed under on acceptance. Must lie inside the issuer's delegated subtree." }, positions: { label: "Placement Positions", - help: "Position names assigned on acceptance (ADR-0105 D8). Every position's permission sets must be allowlisted by the issuer's adminScope." + help: "Position names assigned on acceptance. Every position's permission sets must be allowlisted by the issuer's adminScope." } }, _views: { @@ -848,7 +848,7 @@ export const enObjects: NonNullable = { sys_business_unit: { label: "Business Unit", pluralLabel: "Business Units", - description: "Canonical Business Unit tree — hierarchical org/data-partition node (company / division / department / region / office). ADR-0057 D2.", + description: "Your company structure — divisions, departments, regions and offices — used to scope data and reports.", fields: { name: { label: "Name" @@ -1797,7 +1797,7 @@ export const enObjects: NonNullable = { }, domain_verified: { label: "Domain Verified", - help: "Whether DNS ownership of the email domain has been proven (ADR-0135 D6). Set by “Verify Domain” after the DNS TXT record resolves. Managed by better-auth — not directly editable. Only enforced when domain verification is enabled for the environment." + help: "Whether DNS ownership of the email domain has been proven. Set by “Verify Domain” after the DNS TXT record resolves. Managed by better-auth — not directly editable. Only enforced when domain verification is enabled for the environment." }, oidc_config: { label: "OIDC Config", @@ -2356,7 +2356,7 @@ export const enObjects: NonNullable = { sys_notification: { label: "Notification Event", pluralLabel: "Notification Events", - description: "Notification events — one row per emit() (ADR-0030 Layer 2 ingress)", + description: "Every notification the platform sent, one row per event.", fields: { id: { label: "Notification ID" @@ -2984,7 +2984,7 @@ export const enObjects: NonNullable = { }, environment_id: { label: "Environment (deprecated)", - help: "DEPRECATED. Overlays are environment rows (ADR-0131 D6); no tenant column." + help: "DEPRECATED. Overlays are environment rows; no tenant column." }, version: { label: "Version" @@ -3090,7 +3090,7 @@ export const enObjects: NonNullable = { sys_metadata_audit: { label: "Metadata Audit", pluralLabel: "Metadata Audit", - description: "Append-only audit trail of metadata write decisions (ADR-0010).", + description: "Append-only audit trail of metadata write decisions.", fields: { id: { label: "ID" diff --git a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts index 57cd574eb7b..1827a2961e0 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts @@ -35,7 +35,7 @@ export const esESObjects: NonNullable = { }, role: { label: "Rol de plataforma", - help: "Valor de rol heredado de better-auth (admin, user, …). ObjectStack ya no lo escribe (ADR-0068 D2). Para conceder la condición de administrador de la plataforma, incluya el correo verificado del usuario en `OS_PLATFORM_OWNER_EMAIL`; bajo la postura de tenencia `single`, una asignación sin ámbito de `admin_full_access` en `sys_user_permission_set` también la concede." + help: "Valor de rol heredado de better-auth (admin, user, …). ObjectStack ya no lo escribe. Para conceder la condición de administrador de la plataforma, incluya el correo verificado del usuario en `OS_PLATFORM_OWNER_EMAIL`; bajo la postura de tenencia `single`, una asignación sin ámbito de `admin_full_access` en `sys_user_permission_set` también la concede." }, banned: { label: "Bloqueado", @@ -97,11 +97,11 @@ export const esESObjects: NonNullable = { }, manager_id: { label: "Gerente", - help: "Gerente directo de este usuario. Forma la cadena de reporte que recorre el ámbito jerárquico `own_and_reports` (ADR-0057 / @objectstack/security-enterprise)." + help: "Gerente directo de este usuario. Forma la cadena de reporte que recorre el ámbito jerárquico `own_and_reports`." }, primary_business_unit_id: { label: "Unidad de negocio principal", - help: "Unidad de negocio principal del usuario: una proyección desnormalizada de sys_business_unit_member.is_primary que mantiene plugin-sharing (anexo D12 de ADR-0057). Permite que una búsqueda de usuarios filtre candidatos por unidad de negocio sin recorrer la tabla de unión de membresías. No la edites directamente; establécela mediante la membresía a la unidad de negocio." + help: "Unidad de negocio principal del usuario: una proyección desnormalizada de sys_business_unit_member.is_primary que mantiene plugin-sharing. Permite que una búsqueda de usuarios filtre candidatos por unidad de negocio sin recorrer la tabla de unión de membresías. No la edites directamente; establécela mediante la membresía a la unidad de negocio." }, source: { label: "Origen de la identidad", @@ -537,7 +537,7 @@ export const esESObjects: NonNullable = { }, parent_organization_id: { label: "Organización principal", - help: "Organización principal a efectos de informes y agrupación. No concede NADA: la visibilidad entre organizaciones procede de la membresía, nunca de esta referencia (ADR-0105 D6)." + help: "Organización principal a efectos de informes y agrupación. No concede NADA: la visibilidad entre organizaciones procede de la membresía, nunca de esta referencia." }, sort_order: { label: "Orden", @@ -710,11 +710,11 @@ export const esESObjects: NonNullable = { }, business_unit_id: { label: "Unidad de negocio de asignación", - help: "Unidad de negocio en la que se asigna a la persona invitada al aceptar (ADR-0105 D8). Debe estar dentro del subárbol delegado de quien emite la invitación." + help: "Unidad de negocio en la que se asigna a la persona invitada al aceptar. Debe estar dentro del subárbol delegado de quien emite la invitación." }, positions: { label: "Puestos de asignación", - help: "Nombres de puestos asignados al aceptar (ADR-0105 D8). Los conjuntos de permisos de cada puesto deben estar en la lista de permitidos del adminScope de quien emite la invitación." + help: "Nombres de puestos asignados al aceptar. Los conjuntos de permisos de cada puesto deben estar en la lista de permitidos del adminScope de quien emite la invitación." } }, _views: { @@ -848,7 +848,7 @@ export const esESObjects: NonNullable = { sys_business_unit: { label: "Unidad de negocio", pluralLabel: "Unidades de negocio", - description: "Nodo jerárquico de la estructura organizativa (departamento / división / unidad de negocio / oficina).", + description: "La estructura de su empresa (divisiones, departamentos, regiones y oficinas), usada para delimitar datos e informes.", fields: { name: { label: "Nombre" @@ -1797,7 +1797,7 @@ export const esESObjects: NonNullable = { }, domain_verified: { label: "Dominio verificado", - help: "Si se ha probado la propiedad DNS del dominio de correo (ADR-0135 D6). Lo establece «Verificar dominio» tras resolverse el registro DNS TXT. Gestionado por better-auth: no editable directamente. Solo se aplica cuando la verificación de dominio está habilitada para el entorno." + help: "Si se ha probado la propiedad DNS del dominio de correo. Lo establece «Verificar dominio» tras resolverse el registro DNS TXT. Gestionado por better-auth: no editable directamente. Solo se aplica cuando la verificación de dominio está habilitada para el entorno." }, oidc_config: { label: "Configuración OIDC", @@ -2356,7 +2356,7 @@ export const esESObjects: NonNullable = { sys_notification: { label: "Notificación", pluralLabel: "Notificaciones", - description: "Eventos de notificación: una fila por cada emit() (entrada de la capa 2 de ADR-0030)", + description: "Todas las notificaciones que envió la plataforma, una fila por evento.", fields: { id: { label: "ID de notificación" @@ -2984,7 +2984,7 @@ export const esESObjects: NonNullable = { }, environment_id: { label: "Entorno (obsoleto)", - help: "OBSOLETO. Las personalizaciones son filas del entorno (ADR-0131 D6); no hay columna de tenant." + help: "OBSOLETO. Las personalizaciones son filas del entorno; no hay columna de tenant." }, version: { label: "Versión" @@ -3090,7 +3090,7 @@ export const esESObjects: NonNullable = { sys_metadata_audit: { label: "Auditoría de metadatos", pluralLabel: "Auditoría de metadatos", - description: "Registro de auditoría de solo adición de las decisiones de escritura de metadatos (ADR-0010).", + description: "Registro de auditoría de solo adición de las decisiones de escritura de metadatos.", fields: { id: { label: "ID" diff --git a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts index 443b6c0bbb1..9e7a2062b63 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts @@ -35,7 +35,7 @@ export const jaJPObjects: NonNullable = { }, role: { label: "プラットフォームロール", - help: "レガシーな better-auth ロールスカラー値(admin、user など)。ObjectStack はもはやこの値を書き込みません(ADR-0068 D2)。プラットフォーム管理者権限を付与するには、そのユーザーの確認済みメールアドレスを `OS_PLATFORM_OWNER_EMAIL` に列挙してください。`single` テナンシー態勢では、`sys_user_permission_set` 内のスコープ指定なしの `admin_full_access` 割り当てでも同様にこの権限が付与されます。" + help: "レガシーな better-auth ロールスカラー値(admin、user など)。ObjectStack はもはやこの値を書き込みません。プラットフォーム管理者権限を付与するには、そのユーザーの確認済みメールアドレスを `OS_PLATFORM_OWNER_EMAIL` に列挙してください。`single` テナンシー態勢では、`sys_user_permission_set` 内のスコープ指定なしの `admin_full_access` 割り当てでも同様にこの権限が付与されます。" }, banned: { label: "利用停止", @@ -97,11 +97,11 @@ export const jaJPObjects: NonNullable = { }, manager_id: { label: "マネージャー", - help: "このユーザーの直属のマネージャー。`own_and_reports` 階層スコープがたどるレポートラインを形成します(ADR-0057 / @objectstack/security-enterprise)。" + help: "このユーザーの直属のマネージャー。`own_and_reports` 階層スコープがたどるレポートラインを形成します。" }, primary_business_unit_id: { label: "主所属ビジネスユニット", - help: "ユーザーの主所属ビジネスユニット — sys_business_unit_member.is_primary を非正規化した投影で、plugin-sharing が管理します(ADR-0057 補遺 D12)。ユーザー参照の候補を、メンバーシップの中間テーブルをたどらずにビジネスユニットで絞り込めるようにします。直接編集せず、ビジネスユニットのメンバーシップで設定してください。" + help: "ユーザーの主所属ビジネスユニット — sys_business_unit_member.is_primary を非正規化した投影で、plugin-sharing が管理します。ユーザー参照の候補を、メンバーシップの中間テーブルをたどらずにビジネスユニットで絞り込めるようにします。直接編集せず、ビジネスユニットのメンバーシップで設定してください。" }, source: { label: "ID ソース", @@ -537,7 +537,7 @@ export const jaJPObjects: NonNullable = { }, parent_organization_id: { label: "親組織", - help: "レポート/グループ化のための親。何の権限も付与しません — 組織をまたぐ可視性はメンバーシップから生じ、この参照からは決して生じません(ADR-0105 D6)。" + help: "レポート/グループ化のための親。何の権限も付与しません — 組織をまたぐ可視性はメンバーシップから生じ、この参照からは決して生じません。" }, sort_order: { label: "並び順", @@ -710,11 +710,11 @@ export const jaJPObjects: NonNullable = { }, business_unit_id: { label: "配属先ビジネスユニット", - help: "承認時に招待されたユーザーが配属されるビジネスユニット(ADR-0105 D8)。発行者の委任されたサブツリー内にある必要があります。" + help: "承認時に招待されたユーザーが配属されるビジネスユニット。発行者の委任されたサブツリー内にある必要があります。" }, positions: { label: "配属先ポジション", - help: "承認時に割り当てられるポジション名(ADR-0105 D8)。各ポジションの権限セットは、発行者の adminScope で許可されている必要があります。" + help: "承認時に割り当てられるポジション名。各ポジションの権限セットは、発行者の adminScope で許可されている必要があります。" } }, _views: { @@ -848,7 +848,7 @@ export const jaJPObjects: NonNullable = { sys_business_unit: { label: "ビジネスユニット", pluralLabel: "ビジネスユニット", - description: "階層的な組織ツリーノード(部門 / 事業部 / ビジネスユニット / オフィス)。", + description: "会社の組織構造(事業部、部門、地域、オフィス)。データとレポートの範囲指定に使われます。", fields: { name: { label: "名前" @@ -1797,7 +1797,7 @@ export const jaJPObjects: NonNullable = { }, domain_verified: { label: "ドメイン検証済み", - help: "メールドメインの DNS 所有権が証明済みかどうか(ADR-0135 D6)。DNS TXT レコードが解決した後、「ドメインを検証」により設定されます。better-auth が管理し、直接編集はできません。環境でドメイン検証が有効な場合にのみ適用されます。" + help: "メールドメインの DNS 所有権が証明済みかどうか。DNS TXT レコードが解決した後、「ドメインを検証」により設定されます。better-auth が管理し、直接編集はできません。環境でドメイン検証が有効な場合にのみ適用されます。" }, oidc_config: { label: "OIDC 設定", @@ -2356,7 +2356,7 @@ export const jaJPObjects: NonNullable = { sys_notification: { label: "通知", pluralLabel: "通知", - description: "通知イベント — emit() 1 回につき 1 行(ADR-0030 レイヤー 2 の入口)", + description: "プラットフォームが送信したすべての通知。イベントごとに 1 行です。", fields: { id: { label: "通知 ID" @@ -2984,7 +2984,7 @@ export const jaJPObjects: NonNullable = { }, environment_id: { label: "環境(非推奨)", - help: "非推奨。オーバーレイは環境の行です(ADR-0131 D6)。テナント列はありません。" + help: "非推奨。オーバーレイは環境の行です。テナント列はありません。" }, version: { label: "バージョン" @@ -3090,7 +3090,7 @@ export const jaJPObjects: NonNullable = { sys_metadata_audit: { label: "メタデータ監査", pluralLabel: "メタデータ監査", - description: "メタデータ書き込み決定の追記専用監査証跡(ADR-0010)。", + description: "メタデータ書き込み決定の追記専用監査証跡。", fields: { id: { label: "ID" diff --git a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts index 844278c9c5d..5991eaedbda 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts @@ -35,7 +35,7 @@ export const zhCNObjects: NonNullable = { }, role: { label: "平台角色", - help: "遗留的 better-auth 角色标量(admin、user 等)。ObjectStack 不再写入该字段(ADR-0068 D2)。要授予平台管理员身份,请将该用户已验证的邮箱加入 `OS_PLATFORM_OWNER_EMAIL`;在 `single` 租户态势下,`sys_user_permission_set` 中一条未限定范围的 `admin_full_access` 授权同样可以赋予该身份。" + help: "遗留的 better-auth 角色标量(admin、user 等)。ObjectStack 不再写入该字段。要授予平台管理员身份,请将该用户已验证的邮箱加入 `OS_PLATFORM_OWNER_EMAIL`;在 `single` 租户态势下,`sys_user_permission_set` 中一条未限定范围的 `admin_full_access` 授权同样可以赋予该身份。" }, banned: { label: "已封禁", @@ -97,11 +97,11 @@ export const zhCNObjects: NonNullable = { }, manager_id: { label: "经理", - help: "该用户的直属上级。构成 `own_and_reports` 层级范围所遍历的汇报链(ADR-0057 / @objectstack/security-enterprise)。" + help: "该用户的直属上级。构成 `own_and_reports` 层级范围所遍历的汇报链。" }, primary_business_unit_id: { label: "主属业务单元", - help: "该用户的主属业务单元——sys_business_unit_member.is_primary 的去规范化投影,由 plugin-sharing 维护(ADR-0057 附录 D12)。使用户查找字段无需遍历成员关系关联表即可按业务单元筛选候选人。请勿直接编辑;请通过业务单元成员关系设置。" + help: "该用户的主属业务单元——sys_business_unit_member.is_primary 的去规范化投影,由 plugin-sharing 维护。使用户查找字段无需遍历成员关系关联表即可按业务单元筛选候选人。请勿直接编辑;请通过业务单元成员关系设置。" }, source: { label: "身份来源", @@ -537,7 +537,7 @@ export const zhCNObjects: NonNullable = { }, parent_organization_id: { label: "上级组织", - help: "用于汇报 / 分组的上级。不授予任何权限——跨组织的可见性来自成员关系,绝不来自此引用(ADR-0105 D6)。" + help: "用于汇报 / 分组的上级。不授予任何权限——跨组织的可见性来自成员关系,绝不来自此引用。" }, sort_order: { label: "排序", @@ -710,11 +710,11 @@ export const zhCNObjects: NonNullable = { }, business_unit_id: { label: "分配业务单元", - help: "受邀者接受邀请后被分配到的业务单元(ADR-0105 D8)。必须位于邀请发起人的委派子树内。" + help: "受邀者接受邀请后被分配到的业务单元。必须位于邀请发起人的委派子树内。" }, positions: { label: "分配岗位", - help: "接受邀请时分配的岗位名称(ADR-0105 D8)。每个岗位的权限集都必须在邀请发起人的 adminScope 允许列表内。" + help: "接受邀请时分配的岗位名称。每个岗位的权限集都必须在邀请发起人的 adminScope 允许列表内。" } }, _views: { @@ -848,7 +848,7 @@ export const zhCNObjects: NonNullable = { sys_business_unit: { label: "业务单元", pluralLabel: "业务单元", - description: "层级化组织骨架节点(部门 / 事业部 / 业务单元 / 办公地点)。", + description: "公司的组织结构——事业部、部门、区域和办公地点——用于划定数据和报表的范围。", fields: { name: { label: "名称" @@ -1797,7 +1797,7 @@ export const zhCNObjects: NonNullable = { }, domain_verified: { label: "域名已验证", - help: "是否已证明对该邮箱域名的 DNS 所有权(ADR-0135 D6)。在 DNS TXT 记录解析后由“验证域名”设置。由 better-auth 管理——不可直接编辑。仅在本环境启用域名验证时强制执行。" + help: "是否已证明对该邮箱域名的 DNS 所有权。在 DNS TXT 记录解析后由“验证域名”设置。由 better-auth 管理——不可直接编辑。仅在本环境启用域名验证时强制执行。" }, oidc_config: { label: "OIDC 配置", @@ -2356,7 +2356,7 @@ export const zhCNObjects: NonNullable = { sys_notification: { label: "通知", pluralLabel: "通知", - description: "通知事件——每次 emit() 产生一行(ADR-0030 第 2 层入口)", + description: "平台发出的每一条通知,每个事件一行。", fields: { id: { label: "通知 ID" @@ -2984,7 +2984,7 @@ export const zhCNObjects: NonNullable = { }, environment_id: { label: "环境(已废弃)", - help: "已废弃。覆盖层是环境级的行(ADR-0131 D6),没有租户列。" + help: "已废弃。覆盖层是环境级的行,没有租户列。" }, version: { label: "版本" @@ -3090,7 +3090,7 @@ export const zhCNObjects: NonNullable = { sys_metadata_audit: { label: "元数据审计", pluralLabel: "元数据审计", - description: "元数据写入决策的仅追加审计记录(ADR-0010)。", + description: "元数据写入决策的仅追加审计记录。", fields: { id: { label: "ID" diff --git a/packages/platform-objects/src/audit/sys-notification.object.ts b/packages/platform-objects/src/audit/sys-notification.object.ts index 5da270eb7ce..a9f574497c8 100644 --- a/packages/platform-objects/src/audit/sys-notification.object.ts +++ b/packages/platform-objects/src/audit/sys-notification.object.ts @@ -39,7 +39,9 @@ export const SysNotification = ObjectSchema.create({ class: 'telemetry', retention: { maxAge: '90d' }, }, - description: 'Notification events — one row per emit() (ADR-0030 Layer 2 ingress)', + // Admin-facing copy. The design reference (one row per `emit()`, the + // ADR-0030 Layer 2 ingress) lives in the docblock above. + description: 'Every notification the platform sent, one row per event.', displayNameField: 'topic', nameField: 'topic', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{topic}', diff --git a/packages/platform-objects/src/identity/sys-business-unit.object.ts b/packages/platform-objects/src/identity/sys-business-unit.object.ts index 03f1ce69b29..a46eed560e3 100644 --- a/packages/platform-objects/src/identity/sys-business-unit.object.ts +++ b/packages/platform-objects/src/identity/sys-business-unit.object.ts @@ -28,7 +28,10 @@ export const SysBusinessUnit = ObjectSchema.create({ icon: 'building', isSystem: true, managedBy: 'platform', - description: 'Canonical Business Unit tree — hierarchical org/data-partition node (company / division / department / region / office). ADR-0057 D2.', + // Admin-facing copy: what the records are and what an admin does here. The + // design reference (canonical org/data-partition tree, ADR-0057 D2) lives in + // the docblock above, never in this string. + description: 'Your company structure — divisions, departments, regions and offices — used to scope data and reports.', displayNameField: 'name', nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) titleFormat: '{name}', diff --git a/packages/platform-objects/src/identity/sys-invitation.object.ts b/packages/platform-objects/src/identity/sys-invitation.object.ts index 5f9d2ecf5a7..83bcc361299 100644 --- a/packages/platform-objects/src/identity/sys-invitation.object.ts +++ b/packages/platform-objects/src/identity/sys-invitation.object.ts @@ -295,14 +295,14 @@ export const SysInvitation = ObjectSchema.create({ label: 'Placement Business Unit', required: false, description: - 'Business unit the invitee is placed under on acceptance (ADR-0105 D8). Must lie inside the issuer\'s delegated subtree.', + 'Business unit the invitee is placed under on acceptance. Must lie inside the issuer\'s delegated subtree.', }), positions: Field.json({ label: 'Placement Positions', required: false, description: - 'Position names assigned on acceptance (ADR-0105 D8). Every position\'s permission sets must be allowlisted by the issuer\'s adminScope.', + 'Position names assigned on acceptance. Every position\'s permission sets must be allowlisted by the issuer\'s adminScope.', }), }, diff --git a/packages/platform-objects/src/identity/sys-organization.object.ts b/packages/platform-objects/src/identity/sys-organization.object.ts index 3baca62dc3a..8935bb7304f 100644 --- a/packages/platform-objects/src/identity/sys-organization.object.ts +++ b/packages/platform-objects/src/identity/sys-organization.object.ts @@ -334,7 +334,7 @@ export const SysOrganization = ObjectSchema.create({ required: false, group: 'Group Structure', description: - 'Reporting/grouping parent. Grants NOTHING — visibility across organizations comes from membership, never from this reference (ADR-0105 D6).', + 'Reporting/grouping parent. Grants NOTHING — visibility across organizations comes from membership, never from this reference.', }), sort_order: Field.number({ diff --git a/packages/platform-objects/src/identity/sys-sso-provider.object.ts b/packages/platform-objects/src/identity/sys-sso-provider.object.ts index 3f3273d6666..0c734b0accf 100644 --- a/packages/platform-objects/src/identity/sys-sso-provider.object.ts +++ b/packages/platform-objects/src/identity/sys-sso-provider.object.ts @@ -288,8 +288,9 @@ export const SysSsoProvider = ObjectSchema.create({ label: 'Domain Verified', defaultValue: false, readonly: true, + // ADR-0135 D6 — domain verification (see the actions below). description: - 'Whether DNS ownership of the email domain has been proven (ADR-0135 D6). Set by “Verify Domain” after the DNS TXT record resolves. Managed by better-auth — not directly editable. Only enforced when domain verification is enabled for the environment.', + 'Whether DNS ownership of the email domain has been proven. Set by “Verify Domain” after the DNS TXT record resolves. Managed by better-auth — not directly editable. Only enforced when domain verification is enabled for the environment.', group: 'Identity', }), diff --git a/packages/platform-objects/src/identity/sys-user.object.ts b/packages/platform-objects/src/identity/sys-user.object.ts index 03b402b256e..ebcb8cef97d 100644 --- a/packages/platform-objects/src/identity/sys-user.object.ts +++ b/packages/platform-objects/src/identity/sys-user.object.ts @@ -761,8 +761,9 @@ export const SysUser = ObjectSchema.create({ readonly: true, // ADR-0092 — never the edit form; no writer since #9968 (platform admin: `sys_user_permission_set` / `admin_full_access`) maxLength: 64, group: 'Admin', + // ADR-0068 D2 stopped writing this scalar; the reference stays here, not in the copy. description: - 'Legacy better-auth role scalar (admin, user, …). ObjectStack no longer writes it (ADR-0068 D2). To grant platform-admin standing, list the user\'s verified email in `OS_PLATFORM_OWNER_EMAIL`; under the `single` tenancy posture an unscoped `admin_full_access` assignment in `sys_user_permission_set` also confers it.', + 'Legacy better-auth role scalar (admin, user, …). ObjectStack no longer writes it. To grant platform-admin standing, list the user\'s verified email in `OS_PLATFORM_OWNER_EMAIL`; under the `single` tenancy posture an unscoped `admin_full_access` assignment in `sys_user_permission_set` also confers it.', }), banned: Field.boolean({ @@ -975,7 +976,8 @@ export const SysUser = ObjectSchema.create({ required: false, readonly: true, // ADR-0092 — drives own_and_reports RLS scope; org-structure maintenance is its own surface group: 'Organization', - description: "This user's direct manager. Forms the reporting chain the `own_and_reports` hierarchy scope walks (ADR-0057 / @objectstack/security-enterprise).", + // ADR-0057 / @objectstack/security-enterprise walk this chain. + description: "This user's direct manager. Forms the reporting chain the `own_and_reports` hierarchy scope walks.", }), primary_business_unit_id: Field.lookup('sys_business_unit', { @@ -983,7 +985,8 @@ export const SysUser = ObjectSchema.create({ required: false, readonly: true, // ADR-0092 — denormalised projection maintained by plugin-sharing; never hand-edited group: 'Organization', - description: "The user's primary business unit — a denormalised projection of sys_business_unit_member.is_primary, maintained by plugin-sharing (ADR-0057 addendum D12). Lets a user-lookup filter candidates by business unit without traversing the membership junction. Do not edit directly; set it via business-unit membership.", + // ADR-0057 addendum D12 — the projection's design reference. + description: "The user's primary business unit — a denormalised projection of sys_business_unit_member.is_primary, maintained by plugin-sharing. Lets a user-lookup filter candidates by business unit without traversing the membership junction. Do not edit directly; set it via business-unit membership.", }), // ── System (auto-managed, hidden from create/edit forms) ───── diff --git a/packages/platform-objects/src/system/sys-metadata-activation.object.ts b/packages/platform-objects/src/system/sys-metadata-activation.object.ts index 616b93dcaa9..417872272bb 100644 --- a/packages/platform-objects/src/system/sys-metadata-activation.object.ts +++ b/packages/platform-objects/src/system/sys-metadata-activation.object.ts @@ -70,8 +70,9 @@ export const SysMetadataActivation = ObjectSchema.create({ icon: 'badge-check', isSystem: true, managedBy: 'engine-owned', + // ADR-0126 §4 — the reference lives in the docblock above, not in the copy. description: - 'Activation ledger for metadata artifacts (ADR-0126 §4): one row per artifact whose armed state has been changed from its default. No row means the default — active. Packaged artifacts and positions or permission sets authored in this environment are both recorded here.', + 'Activation ledger for metadata artifacts: one row per artifact whose armed state has been changed from its default. No row means the default — active. Packaged artifacts and positions or permission sets authored in this environment are both recorded here.', displayNameField: 'name', nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) highlightFields: ['metadata_type', 'name', 'package_id', 'active'], diff --git a/packages/services/service-messaging/src/objects/admin-copy-vocabulary.test.ts b/packages/services/service-messaging/src/objects/admin-copy-vocabulary.test.ts new file mode 100644 index 00000000000..59b7f803aac --- /dev/null +++ b/packages/services/service-messaging/src/objects/admin-copy-vocabulary.test.ts @@ -0,0 +1,116 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Admin-facing copy of the messaging objects carries no design-record + * vocabulary. + * + * Setup opens four of these objects from its nav (Notification Preferences, + * Subscriptions and Templates, and HTTP Deliveries under Integrations); the + * sentence under each page title is the object's `description`, a field's + * help tooltip is the field's `description`, and the generated bundles carry + * both into every locale. An ADR number, a decision id ("D2"), a + * review-finding id ("finding F2"), an API call ("emit()") or a runtime + * internal ("kernel") belongs in the docblock next to the string, never in the + * string. Every object this package ships is judged, not only the four Setup + * opens: a nav entry added later inherits the rule with nothing to update. + * + * The same rule over `@objectstack/platform-objects`' own objects and the + * Setup nav lives in that package (`src/apps/setup-copy-vocabulary.test.ts`); + * the matcher is restated rather than imported, because a shared assertion + * module would be one more export for a test to reach across a package for. + */ + +import { describe, expect, it } from 'vitest'; +import * as MessagingObjects from './index.js'; +import { enObjects } from '../translations/en.objects.generated.js'; +import { zhCNObjects } from '../translations/zh-CN.objects.generated.js'; +import { jaJPObjects } from '../translations/ja-JP.objects.generated.js'; +import { esESObjects } from '../translations/es-ES.objects.generated.js'; + +/** The design-record vocabulary an admin-facing string must not carry. */ +const DESIGN_RECORD_VOCABULARY: ReadonlyArray = [ + ['an ADR number', /ADR-\d/], + ['a decision id', /\bD\d+\b/], + ['a review-finding id', /finding F/], + ['an API call', /\bemit\(\)/], + ['a runtime internal', /\bkernel\b/i], +]; + +function vocabularyIn(text: string): string[] { + return DESIGN_RECORD_VOCABULARY.filter(([, re]) => re.test(text)).map(([name]) => name); +} + +const COPY_KEYS = new Set(['label', 'pluralLabel', 'description']); + +function copyStrings(node: unknown, path: string, out: Array<[string, string]>): void { + if (Array.isArray(node)) { + node.forEach((v, i) => copyStrings(v, `${path}[${i}]`, out)); + return; + } + if (!node || typeof node !== 'object') return; + for (const [k, v] of Object.entries(node)) { + const at = path ? `${path}.${k}` : k; + if (typeof v === 'string') { + if (COPY_KEYS.has(k)) out.push([at, v]); + } else { + copyStrings(v, at, out); + } + } +} + +function leafStrings(node: unknown, path: string, out: Array<[string, string]>): void { + if (typeof node === 'string') { + out.push([path, node]); + return; + } + if (!node || typeof node !== 'object') return; + for (const [k, v] of Object.entries(node)) leafStrings(v, path ? `${path}.${k}` : k, out); +} + +function offenders(strings: Array<[string, string]>): string[] { + return strings + .map(([at, text]) => [at, text, vocabularyIn(text)] as const) + .filter(([, , hits]) => hits.length > 0) + .map(([at, text, hits]) => `${at} (${hits.join(', ')}): ${text}`); +} + +type ObjectLike = { name: string; fields: Record }; +function isObjectSchema(v: unknown): v is ObjectLike { + return !!v && typeof v === 'object' + && typeof (v as ObjectLike).name === 'string' + && !!(v as ObjectLike).fields && typeof (v as ObjectLike).fields === 'object'; +} + +const OBJECT_BUNDLES = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects } as const; + +describe('messaging objects: admin-facing copy carries no design-record vocabulary', () => { + it('the matcher recognises each kind of vocabulary it exists to keep out', () => { + expect(vocabularyIn('Delivery outbox (ADR-0030 Layer 4).')).toEqual(['an ADR number']); + expect(vocabularyIn('Placement intent, decision D12.')).toEqual(['a decision id']); + expect(vocabularyIn('Bypass rule (finding F2).')).toEqual(['a review-finding id']); + expect(vocabularyIn('One row per emit().')).toEqual(['an API call']); + expect(vocabularyIn('The Kernel API is additive.')).toEqual(['a runtime internal']); + expect(vocabularyIn('Which topics and channels each user receives.')).toEqual([]); + }); + + it('object, field and view copy of every object this package exports', () => { + const objects: ObjectLike[] = (Object.values(MessagingObjects) as unknown[]).filter(isObjectSchema); + const names = objects.map((o) => o.name); + for (const name of [ + 'sys_notification_preference', 'sys_notification_subscription', 'sys_notification_template', + 'sys_http_delivery', 'sys_notification_delivery', + ]) { + expect(names, `${name} is in the population`).toContain(name); + } + const strings: Array<[string, string]> = []; + for (const def of objects) copyStrings(def, def.name, strings); + expect(offenders(strings)).toEqual([]); + }); + + it.each(Object.keys(OBJECT_BUNDLES))('every leaf of the %s objects bundle', (locale) => { + const strings: Array<[string, string]> = []; + leafStrings(OBJECT_BUNDLES[locale as keyof typeof OBJECT_BUNDLES], 'objects', strings); + expect(strings.length).toBeGreaterThan(100); + expect(offenders(strings)).toEqual([]); + }); +}); diff --git a/packages/services/service-messaging/src/objects/http-delivery.object.ts b/packages/services/service-messaging/src/objects/http-delivery.object.ts index 1e016913fe6..403f8c2b7b2 100644 --- a/packages/services/service-messaging/src/objects/http-delivery.object.ts +++ b/packages/services/service-messaging/src/objects/http-delivery.object.ts @@ -41,8 +41,10 @@ export const HttpDelivery = ObjectSchema.create({ retention: { maxAge: '30d' }, }, userActions: { create: false, edit: false, delete: false, import: false }, - description: - 'Durable outbox row for one outbound-HTTP attempt (ADR-0018). Managed by @objectstack/service-messaging; do not write directly.', + // Admin-facing copy (Setup → Integrations → HTTP Deliveries). The design + // reference — a durable outbox row per outbound-HTTP attempt (ADR-0018 M3), + // written only by this package — lives in the docblock above. + description: 'Outgoing HTTP calls made by webhooks and flows, one row per call, with its delivery status.', // [ADR-0079] The record title is `display_title`, a text formula over the // same two columns `titleFormat` names. The pointer used to be `id`: once a // renderer honours ADR-0079's order (an explicit `nameField` wins over diff --git a/packages/services/service-messaging/src/objects/notification-delivery.object.ts b/packages/services/service-messaging/src/objects/notification-delivery.object.ts index a8e05e2f416..46eca396b27 100644 --- a/packages/services/service-messaging/src/objects/notification-delivery.object.ts +++ b/packages/services/service-messaging/src/objects/notification-delivery.object.ts @@ -69,7 +69,8 @@ export const NotificationDelivery = ObjectSchema.create({ onlyWhen: { status: { $in: ['dead', 'suppressed'] } }, }, }, - description: 'Durable per-recipient × channel delivery outbox (ADR-0030 Layer 4).', + // ADR-0030 Layer 4 — the reference lives in the docblock above, not in the copy. + description: 'Durable per-recipient × channel delivery outbox.', // [ADR-0079] The record title is `display_title`, a text formula over the // same two columns `titleFormat` names. With no pointer declared, the // registry's designate-only pass stamped `nameField: 'id'` (the first diff --git a/packages/services/service-messaging/src/objects/notification-preference.object.ts b/packages/services/service-messaging/src/objects/notification-preference.object.ts index 8f0349a566f..3d8f7e9e96b 100644 --- a/packages/services/service-messaging/src/objects/notification-preference.object.ts +++ b/packages/services/service-messaging/src/objects/notification-preference.object.ts @@ -31,7 +31,9 @@ export const NotificationPreference = ObjectSchema.create({ // global defaults) from the Setup "Notification Preferences" grid. The bucket // default is full CRUD, so no `userActions` block is needed — RLS is the authz. managedBy: 'system-data', - description: 'Per-user × topic × channel notification toggle (mute/allow), with admin-global defaults.', + // Admin-facing copy; the per-user × topic × channel matrix is described in + // the docblock above (ADR-0030 Layer 3). + description: 'Which topics and channels each user receives; admins set the defaults.', // [ADR-0079] The record title is `display_title`, a text formula over the // same three columns `titleFormat` names. With no pointer declared, the // registry's designate-only pass stamped `nameField: 'id'` (the first diff --git a/packages/services/service-messaging/src/objects/notification-template.object.ts b/packages/services/service-messaging/src/objects/notification-template.object.ts index 4431b2f9cf9..7c8f2d09345 100644 --- a/packages/services/service-messaging/src/objects/notification-template.object.ts +++ b/packages/services/service-messaging/src/objects/notification-template.object.ts @@ -27,7 +27,8 @@ export const NotificationTemplate = ObjectSchema.create({ // CRUD, so no `userActions` block is needed — affordance is a declaration // only; permission sets remain the authz. managedBy: 'system-data', - description: 'Per (topic × channel × locale) render template for notifications.', + // Admin-facing copy; one row per (topic, channel, locale) — see the docblock. + description: 'The subject and body each notification topic sends, per channel and language.', titleFormat: '{topic} · {channel} · {locale}', highlightFields: ['topic', 'channel', 'locale', 'is_active'], diff --git a/packages/services/service-messaging/src/translations/en.objects.generated.ts b/packages/services/service-messaging/src/translations/en.objects.generated.ts index c6a4a2a890b..3d046904fe2 100644 --- a/packages/services/service-messaging/src/translations/en.objects.generated.ts +++ b/packages/services/service-messaging/src/translations/en.objects.generated.ts @@ -116,7 +116,7 @@ export const enObjects: NonNullable = { sys_notification_delivery: { label: "Notification Delivery", pluralLabel: "Notification Deliveries", - description: "Durable per-recipient × channel delivery outbox (ADR-0030 Layer 4).", + description: "Durable per-recipient × channel delivery outbox.", fields: { id: { label: "Delivery ID" @@ -190,7 +190,7 @@ export const enObjects: NonNullable = { sys_notification_preference: { label: "Notification Preference", pluralLabel: "Notification Preferences", - description: "Per-user × topic × channel notification toggle (mute/allow), with admin-global defaults.", + description: "Which topics and channels each user receives; admins set the defaults.", fields: { id: { label: "Preference ID" @@ -268,7 +268,7 @@ export const enObjects: NonNullable = { sys_notification_template: { label: "Notification Template", pluralLabel: "Notification Templates", - description: "Per (topic × channel × locale) render template for notifications.", + description: "The subject and body each notification topic sends, per channel and language.", fields: { id: { label: "Template ID" @@ -319,7 +319,7 @@ export const enObjects: NonNullable = { sys_http_delivery: { label: "HTTP Delivery", pluralLabel: "HTTP Deliveries", - description: "Durable outbox row for one outbound-HTTP attempt (ADR-0018). Managed by @objectstack/service-messaging; do not write directly.", + description: "Outgoing HTTP calls made by webhooks and flows, one row per call, with its delivery status.", fields: { id: { label: "Delivery ID", diff --git a/packages/services/service-messaging/src/translations/es-ES.objects.generated.ts b/packages/services/service-messaging/src/translations/es-ES.objects.generated.ts index ecec74cb01e..d02a3fb522e 100644 --- a/packages/services/service-messaging/src/translations/es-ES.objects.generated.ts +++ b/packages/services/service-messaging/src/translations/es-ES.objects.generated.ts @@ -116,7 +116,7 @@ export const esESObjects: NonNullable = { sys_notification_delivery: { label: "Notification Delivery", pluralLabel: "Notification Deliveries", - description: "Durable per-recipient × channel delivery outbox (ADR-0030 Layer 4).", + description: "Durable per-recipient × channel delivery outbox.", fields: { id: { label: "Delivery ID" @@ -190,7 +190,7 @@ export const esESObjects: NonNullable = { sys_notification_preference: { label: "Notification Preference", pluralLabel: "Notification Preferences", - description: "Per-user × topic × channel notification toggle (mute/allow), with admin-global defaults.", + description: "Which topics and channels each user receives; admins set the defaults.", fields: { id: { label: "Preference ID" @@ -268,7 +268,7 @@ export const esESObjects: NonNullable = { sys_notification_template: { label: "Notification Template", pluralLabel: "Notification Templates", - description: "Per (topic × channel × locale) render template for notifications.", + description: "The subject and body each notification topic sends, per channel and language.", fields: { id: { label: "Template ID" @@ -319,7 +319,7 @@ export const esESObjects: NonNullable = { sys_http_delivery: { label: "HTTP Delivery", pluralLabel: "HTTP Deliveries", - description: "Durable outbox row for one outbound-HTTP attempt (ADR-0018). Managed by @objectstack/service-messaging; do not write directly.", + description: "Outgoing HTTP calls made by webhooks and flows, one row per call, with its delivery status.", fields: { id: { label: "Delivery ID", diff --git a/packages/services/service-messaging/src/translations/es-ES.source-hashes.generated.ts b/packages/services/service-messaging/src/translations/es-ES.source-hashes.generated.ts index 5e1ff4495ac..32f3735a653 100644 --- a/packages/services/service-messaging/src/translations/es-ES.source-hashes.generated.ts +++ b/packages/services/service-messaging/src/translations/es-ES.source-hashes.generated.ts @@ -21,7 +21,7 @@ export const esESGeneratedSourceHashes: Readonly> = { "objects.sys_http_delivery._views.failures.label": "c4170eee981cbb84", "objects.sys_http_delivery._views.pending.label": "23b532305c06373f", "objects.sys_http_delivery._views.recent.label": "62d27bb9d0349c99", - "objects.sys_http_delivery.description": "1b5cc18e87fa69ae", + "objects.sys_http_delivery.description": "188ec3e637ec17a4", "objects.sys_http_delivery.fields.attempts.label": "096d2dbc7038a926", "objects.sys_http_delivery.fields.claimed_at.label": "28fa2f052f552d6a", "objects.sys_http_delivery.fields.claimed_by.label": "298232b57e56a870", @@ -59,7 +59,7 @@ export const esESGeneratedSourceHashes: Readonly> = { "objects.sys_http_delivery.label": "302f263363fe99ab", "objects.sys_http_delivery.pluralLabel": "340f81dc6ea64987", "objects.sys_inbox_message.fields.actor_id.label": "b155813f8a7f06e3", - "objects.sys_notification_delivery.description": "6e93eafd2b3ec57a", + "objects.sys_notification_delivery.description": "c18a79f86ae1b06d", "objects.sys_notification_delivery.fields.attempts.label": "096d2dbc7038a926", "objects.sys_notification_delivery.fields.channel.label": "979883fe73ee88cb", "objects.sys_notification_delivery.fields.claimed_at.label": "28fa2f052f552d6a", @@ -91,7 +91,7 @@ export const esESGeneratedSourceHashes: Readonly> = { "objects.sys_notification_delivery.fields.updated_at.label": "aba63dc2a9c79b8d", "objects.sys_notification_delivery.label": "dab3eb6594ba3014", "objects.sys_notification_delivery.pluralLabel": "654247db5abbd7d2", - "objects.sys_notification_preference.description": "560b825951c71086", + "objects.sys_notification_preference.description": "48556455d3292290", "objects.sys_notification_preference.fields.channel.help": "8c06b7c3f1a65fd7", "objects.sys_notification_preference.fields.channel.label": "979883fe73ee88cb", "objects.sys_notification_preference.fields.created_at.label": "1f02d416befb595b", @@ -145,7 +145,7 @@ export const esESGeneratedSourceHashes: Readonly> = { "objects.sys_notification_subscription.fields.topic.label": "819afdb3853e9d80", "objects.sys_notification_subscription.label": "cb96f976a7b36f34", "objects.sys_notification_subscription.pluralLabel": "907cb60dda7b39c8", - "objects.sys_notification_template.description": "159a2c4fa16bf97c", + "objects.sys_notification_template.description": "d4e78f9fe9420a04", "objects.sys_notification_template.fields.body.help": "af87336e946106ce", "objects.sys_notification_template.fields.body.label": "d1753a024d6a9bed", "objects.sys_notification_template.fields.channel.help": "f71964058486f39e", diff --git a/packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts b/packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts index 70813119840..af853010bdd 100644 --- a/packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts +++ b/packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts @@ -116,7 +116,7 @@ export const jaJPObjects: NonNullable = { sys_notification_delivery: { label: "Notification Delivery", pluralLabel: "Notification Deliveries", - description: "Durable per-recipient × channel delivery outbox (ADR-0030 Layer 4).", + description: "Durable per-recipient × channel delivery outbox.", fields: { id: { label: "Delivery ID" @@ -190,7 +190,7 @@ export const jaJPObjects: NonNullable = { sys_notification_preference: { label: "Notification Preference", pluralLabel: "Notification Preferences", - description: "Per-user × topic × channel notification toggle (mute/allow), with admin-global defaults.", + description: "Which topics and channels each user receives; admins set the defaults.", fields: { id: { label: "Preference ID" @@ -268,7 +268,7 @@ export const jaJPObjects: NonNullable = { sys_notification_template: { label: "Notification Template", pluralLabel: "Notification Templates", - description: "Per (topic × channel × locale) render template for notifications.", + description: "The subject and body each notification topic sends, per channel and language.", fields: { id: { label: "Template ID" @@ -319,7 +319,7 @@ export const jaJPObjects: NonNullable = { sys_http_delivery: { label: "HTTP Delivery", pluralLabel: "HTTP Deliveries", - description: "Durable outbox row for one outbound-HTTP attempt (ADR-0018). Managed by @objectstack/service-messaging; do not write directly.", + description: "Outgoing HTTP calls made by webhooks and flows, one row per call, with its delivery status.", fields: { id: { label: "Delivery ID", diff --git a/packages/services/service-messaging/src/translations/ja-JP.source-hashes.generated.ts b/packages/services/service-messaging/src/translations/ja-JP.source-hashes.generated.ts index d708d65c8ee..696feb3c340 100644 --- a/packages/services/service-messaging/src/translations/ja-JP.source-hashes.generated.ts +++ b/packages/services/service-messaging/src/translations/ja-JP.source-hashes.generated.ts @@ -21,7 +21,7 @@ export const jaJPGeneratedSourceHashes: Readonly> = { "objects.sys_http_delivery._views.failures.label": "c4170eee981cbb84", "objects.sys_http_delivery._views.pending.label": "23b532305c06373f", "objects.sys_http_delivery._views.recent.label": "62d27bb9d0349c99", - "objects.sys_http_delivery.description": "1b5cc18e87fa69ae", + "objects.sys_http_delivery.description": "188ec3e637ec17a4", "objects.sys_http_delivery.fields.attempts.label": "096d2dbc7038a926", "objects.sys_http_delivery.fields.claimed_at.label": "28fa2f052f552d6a", "objects.sys_http_delivery.fields.claimed_by.label": "298232b57e56a870", @@ -58,7 +58,7 @@ export const jaJPGeneratedSourceHashes: Readonly> = { "objects.sys_http_delivery.fields.url.label": "501585a180652bdf", "objects.sys_http_delivery.label": "302f263363fe99ab", "objects.sys_http_delivery.pluralLabel": "340f81dc6ea64987", - "objects.sys_notification_delivery.description": "6e93eafd2b3ec57a", + "objects.sys_notification_delivery.description": "c18a79f86ae1b06d", "objects.sys_notification_delivery.fields.attempts.label": "096d2dbc7038a926", "objects.sys_notification_delivery.fields.channel.label": "979883fe73ee88cb", "objects.sys_notification_delivery.fields.claimed_at.label": "28fa2f052f552d6a", @@ -90,7 +90,7 @@ export const jaJPGeneratedSourceHashes: Readonly> = { "objects.sys_notification_delivery.fields.updated_at.label": "aba63dc2a9c79b8d", "objects.sys_notification_delivery.label": "dab3eb6594ba3014", "objects.sys_notification_delivery.pluralLabel": "654247db5abbd7d2", - "objects.sys_notification_preference.description": "560b825951c71086", + "objects.sys_notification_preference.description": "48556455d3292290", "objects.sys_notification_preference.fields.channel.help": "8c06b7c3f1a65fd7", "objects.sys_notification_preference.fields.channel.label": "979883fe73ee88cb", "objects.sys_notification_preference.fields.created_at.label": "1f02d416befb595b", @@ -144,7 +144,7 @@ export const jaJPGeneratedSourceHashes: Readonly> = { "objects.sys_notification_subscription.fields.topic.label": "819afdb3853e9d80", "objects.sys_notification_subscription.label": "cb96f976a7b36f34", "objects.sys_notification_subscription.pluralLabel": "907cb60dda7b39c8", - "objects.sys_notification_template.description": "159a2c4fa16bf97c", + "objects.sys_notification_template.description": "d4e78f9fe9420a04", "objects.sys_notification_template.fields.body.help": "af87336e946106ce", "objects.sys_notification_template.fields.body.label": "d1753a024d6a9bed", "objects.sys_notification_template.fields.channel.help": "f71964058486f39e", diff --git a/packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts b/packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts index b9aa60e60e8..352f0e15aaa 100644 --- a/packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts +++ b/packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts @@ -116,7 +116,7 @@ export const zhCNObjects: NonNullable = { sys_notification_delivery: { label: "通知投递", pluralLabel: "通知投递", - description: "持久化的按接收人 × 通道投递发件箱(ADR-0030 第 4 层)。", + description: "持久化的按接收人 × 通道投递发件箱。", fields: { id: { label: "投递 ID" @@ -190,7 +190,7 @@ export const zhCNObjects: NonNullable = { sys_notification_preference: { label: "通知偏好", pluralLabel: "通知偏好", - description: "按用户 × 主题 × 通道的通知开关(静音/允许),支持管理员全局默认值。", + description: "每位用户接收哪些通知主题和通道;默认值由管理员设置。", fields: { id: { label: "偏好 ID" @@ -268,7 +268,7 @@ export const zhCNObjects: NonNullable = { sys_notification_template: { label: "通知模板", pluralLabel: "通知模板", - description: "按(主题 × 通道 × 语言)的通知渲染模板。", + description: "每个通知主题在各通道、各语言下发送的标题和正文。", fields: { id: { label: "模板 ID" @@ -319,7 +319,7 @@ export const zhCNObjects: NonNullable = { sys_http_delivery: { label: "HTTP 投递", pluralLabel: "HTTP 投递", - description: "单次出站 HTTP 调用的持久化发件箱行(ADR-0018)。由 @objectstack/service-messaging 管理,请勿直接写入。", + description: "由 Webhook 和流程发出的 HTTP 调用,每次调用一行,并记录其投递状态。", fields: { id: { label: "投递 ID",