diff --git a/.changeset/23829-setup-copy-services.md b/.changeset/23829-setup-copy-services.md new file mode 100644 index 00000000000..2842d8563f5 --- /dev/null +++ b/.changeset/23829-setup-copy-services.md @@ -0,0 +1,22 @@ +--- +'@objectstack/plugin-approvals': patch +'@objectstack/plugin-security': patch +'@objectstack/service-automation': patch +'@objectstack/plugin-sharing': patch +--- + +Clause-②: no + +The approval, security, automation and sharing objects describe their records in plain admin +sentences. Approval Delegations now reads "While someone is away, approvals assigned to them +personally go to their delegate.", Record Shares "Records shared beyond their owner, one row per +grant.", User Positions "Which positions each user holds, one row per assignment.", Audience +Binding Suggestions "A package's suggestion to give one of its permission sets to everyone who +signs in, and the decision taken on it." and Automation Runs "Paused automation runs waiting to +resume, and the recent history of finished runs." Field help on Approval Requests, Approval +Delegations, User Positions, User Permission Sets and Audience Binding Suggestions, the +descriptions of the `organization_admin_no_bypass`, `mcp_agent_data_read` and +`mcp_agent_data_write` permission sets, the flow-dispatch ledger's description and the +reserved-position refusal no longer carry design-record numbers or runtime vocabulary; 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/plugins/plugin-approvals/src/admin-copy-vocabulary.test.ts b/packages/plugins/plugin-approvals/src/admin-copy-vocabulary.test.ts new file mode 100644 index 00000000000..e1afe13877d --- /dev/null +++ b/packages/plugins/plugin-approvals/src/admin-copy-vocabulary.test.ts @@ -0,0 +1,120 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Admin-facing copy of the approval objects carries no design-record + * vocabulary. + * + * Setup opens three of these objects from its Approvals group (Requests, + * Action History and Delegations); 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 three Setup opens: a nav entry added later + * inherits the rule with nothing to update. + * + * The same rule over the other Setup producers lives in each of them + * (`@objectstack/platform-objects`' `src/apps/setup-copy-vocabulary.test.ts`, + * `@objectstack/service-messaging`'s `src/objects/admin-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 Approvals from './index.js'; +// Registered by the plugin, not re-exported by the package entry. +import { SysApprovalToken } from './sys-approval-token.object.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('approval objects: admin-facing copy carries no design-record vocabulary', () => { + it('the matcher recognises each kind of vocabulary it exists to keep out', () => { + expect(vocabularyIn('Suspended run this request gates (ADR-0019).')).toEqual(['an ADR number']); + expect(vocabularyIn('Enforced by the shared predicate, decision D2.')).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('Resumed by a rebuilt kernel.')).toEqual(['a runtime internal']); + expect(vocabularyIn('While someone is away, approvals go to their delegate.')).toEqual([]); + }); + + it('object, field and view copy of every object this package ships', () => { + const objects: ObjectLike[] = [...(Object.values(Approvals) as unknown[]), SysApprovalToken] + .filter(isObjectSchema); + const names = objects.map((o) => o.name); + for (const name of [ + 'sys_approval_request', 'sys_approval_action', 'sys_approval_approver', + 'sys_approval_token', 'sys_approval_delegation', + ]) { + 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/plugins/plugin-approvals/src/sys-approval-delegation.object.ts b/packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts index 15b36036927..11f178f3e70 100644 --- a/packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts +++ b/packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts @@ -39,8 +39,11 @@ export const SysApprovalDelegation = ObjectSchema.create({ // CRUD (matching the full-CRUD apiMethods below), so no `userActions` block is // needed — RLS/permission sets are the authz. managedBy: 'system-data', - description: - 'Self-service out-of-office rule: route this user\'s approver slots to a delegate within a time window.', + // The sentence Setup shows under the "Delegations (OOO)" title. It says + // "assigned to them personally" because only individually-routed approvers + // (`user` / `field` / `manager`) are rerouted; team, department, position and + // membership-level approvers are not (`ApprovalService.expandApprovers`). + description: 'While someone is away, approvals assigned to them personally go to their delegate.', // [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 @@ -97,13 +100,14 @@ export const SysApprovalDelegation = ObjectSchema.create({ description: 'The backup who receives the delegator\'s approvals while this rule is active. Acts under their own identity.', }), + // [ADR-0091 D2] `isGrantActive` is the shared validity predicate. valid_from: Field.datetime({ label: 'Valid From', required: false, group: 'Delegation', description: 'Rule is inactive before this instant. Null = active immediately. ' + - 'Enforced at resolution time via isGrantActive (ADR-0091 D2 predicate) — never by a background job.', + 'Enforced at resolution time via isGrantActive — never by a background job.', }), valid_until: Field.datetime({ diff --git a/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts b/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts index eb3148ea79d..f3a1f863742 100644 --- a/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts +++ b/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts @@ -330,7 +330,7 @@ export const SysApprovalRequest = ObjectSchema.create({ required: false, maxLength: 100, readonly: true, - description: 'Suspended automation run id this request gates (ADR-0019). The decision resumes it.', + description: 'Suspended automation run id this request gates. The decision resumes it.', group: 'State', }), @@ -339,7 +339,7 @@ export const SysApprovalRequest = ObjectSchema.create({ required: false, maxLength: 100, readonly: true, - description: 'Approval node id within the flow that opened this request (ADR-0019).', + description: 'Approval node id within the flow that opened this request.', group: 'State', }), @@ -347,7 +347,7 @@ export const SysApprovalRequest = ObjectSchema.create({ label: 'Node Config', required: false, readonly: true, - description: 'Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019).', + description: 'Snapshot of the Approval node config (approvers/behavior) for node-driven requests.', group: 'State', }), diff --git a/packages/plugins/plugin-approvals/src/translations/en.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/en.objects.generated.ts index fbe91fbaceb..d344c13b01c 100644 --- a/packages/plugins/plugin-approvals/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-approvals/src/translations/en.objects.generated.ts @@ -83,15 +83,15 @@ export const enObjects: NonNullable = { }, flow_run_id: { label: "Flow Run", - help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it." + help: "Suspended automation run id this request gates. The decision resumes it." }, flow_node_id: { label: "Flow Node", - help: "Approval node id within the flow that opened this request (ADR-0019)." + help: "Approval node id within the flow that opened this request." }, node_config_json: { label: "Node Config", - help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)." + help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests." }, completed_at: { label: "Completed At" @@ -315,7 +315,7 @@ export const enObjects: NonNullable = { sys_approval_delegation: { label: "Approval Delegation", pluralLabel: "Approval Delegations", - description: "Self-service out-of-office rule: route this user's approver slots to a delegate within a time window.", + description: "While someone is away, approvals assigned to them personally go to their delegate.", fields: { id: { label: "Delegation ID" @@ -334,7 +334,7 @@ export const enObjects: NonNullable = { }, valid_from: { label: "Valid From", - help: "Rule is inactive before this instant. Null = active immediately. Enforced at resolution time via isGrantActive (ADR-0091 D2 predicate) — never by a background job." + help: "Rule is inactive before this instant. Null = active immediately. Enforced at resolution time via isGrantActive — never by a background job." }, valid_until: { label: "Valid Until", diff --git a/packages/plugins/plugin-approvals/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/es-ES.objects.generated.ts index 3d9d71979ba..5f0caa492f2 100644 --- a/packages/plugins/plugin-approvals/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-approvals/src/translations/es-ES.objects.generated.ts @@ -83,15 +83,15 @@ export const esESObjects: NonNullable = { }, flow_run_id: { label: "Flow Run", - help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it." + help: "Suspended automation run id this request gates. The decision resumes it." }, flow_node_id: { label: "Flow Node", - help: "Approval node id within the flow that opened this request (ADR-0019)." + help: "Approval node id within the flow that opened this request." }, node_config_json: { label: "Node Config", - help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)." + help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests." }, completed_at: { label: "Completado el" @@ -315,7 +315,7 @@ export const esESObjects: NonNullable = { sys_approval_delegation: { label: "Approval Delegation", pluralLabel: "Approval Delegations", - description: "Self-service out-of-office rule: route this user's approver slots to a delegate within a time window.", + description: "While someone is away, approvals assigned to them personally go to their delegate.", fields: { id: { label: "Delegation ID" @@ -334,7 +334,7 @@ export const esESObjects: NonNullable = { }, valid_from: { label: "Valid From", - help: "Rule is inactive before this instant. Null = active immediately. Enforced at resolution time via isGrantActive (ADR-0091 D2 predicate) — never by a background job." + help: "Rule is inactive before this instant. Null = active immediately. Enforced at resolution time via isGrantActive — never by a background job." }, valid_until: { label: "Valid Until", diff --git a/packages/plugins/plugin-approvals/src/translations/es-ES.source-hashes.generated.ts b/packages/plugins/plugin-approvals/src/translations/es-ES.source-hashes.generated.ts index beb8628cf46..ebb9da253b0 100644 --- a/packages/plugins/plugin-approvals/src/translations/es-ES.source-hashes.generated.ts +++ b/packages/plugins/plugin-approvals/src/translations/es-ES.source-hashes.generated.ts @@ -22,7 +22,7 @@ export const esESGeneratedSourceHashes: Readonly> = { "objects.sys_approval_delegation._views.active.emptyState.message": "1095d61c017b6db2", "objects.sys_approval_delegation._views.active.emptyState.title": "799d28e51d88963e", "objects.sys_approval_delegation._views.active.label": "eee107cf4e466779", - "objects.sys_approval_delegation.description": "517790acf02a372d", + "objects.sys_approval_delegation.description": "177cd798e5944c83", "objects.sys_approval_delegation.fields.created_at.label": "1f02d416befb595b", "objects.sys_approval_delegation.fields.delegate_id.help": "9cfc641f7b0b04f9", "objects.sys_approval_delegation.fields.delegate_id.label": "afd6d8733dc5bc14", @@ -36,16 +36,16 @@ export const esESGeneratedSourceHashes: Readonly> = { "objects.sys_approval_delegation.fields.reason.help": "712516f1461dff2b", "objects.sys_approval_delegation.fields.reason.label": "ac399f03d7f2e64d", "objects.sys_approval_delegation.fields.updated_at.label": "aba63dc2a9c79b8d", - "objects.sys_approval_delegation.fields.valid_from.help": "01386010c454bfbd", + "objects.sys_approval_delegation.fields.valid_from.help": "20b8f73a1c3976d6", "objects.sys_approval_delegation.fields.valid_from.label": "02eaf201e57c9eda", "objects.sys_approval_delegation.fields.valid_until.help": "a48ea34e65933e9d", "objects.sys_approval_delegation.fields.valid_until.label": "a68db5bd4fc72a13", "objects.sys_approval_delegation.label": "4389c4fe5f5c9746", "objects.sys_approval_delegation.pluralLabel": "f07fec3ffd74e6db", - "objects.sys_approval_request.fields.flow_node_id.help": "154aa23b4eee4cae", + "objects.sys_approval_request.fields.flow_node_id.help": "97e7489e6549d78c", "objects.sys_approval_request.fields.flow_node_id.label": "052ad568aa41227c", - "objects.sys_approval_request.fields.flow_run_id.help": "35c92818f5e11090", + "objects.sys_approval_request.fields.flow_run_id.help": "5718cf2ce8a0abd6", "objects.sys_approval_request.fields.flow_run_id.label": "d0124e323da966f9", - "objects.sys_approval_request.fields.node_config_json.help": "7ee00ef9b3ca7e80", + "objects.sys_approval_request.fields.node_config_json.help": "e832e60ef12791e6", "objects.sys_approval_request.fields.node_config_json.label": "e64eb4bf7d7ea033", }; diff --git a/packages/plugins/plugin-approvals/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/ja-JP.objects.generated.ts index f87e1019fcc..1e4841771a4 100644 --- a/packages/plugins/plugin-approvals/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-approvals/src/translations/ja-JP.objects.generated.ts @@ -83,15 +83,15 @@ export const jaJPObjects: NonNullable = { }, flow_run_id: { label: "Flow Run", - help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it." + help: "Suspended automation run id this request gates. The decision resumes it." }, flow_node_id: { label: "Flow Node", - help: "Approval node id within the flow that opened this request (ADR-0019)." + help: "Approval node id within the flow that opened this request." }, node_config_json: { label: "Node Config", - help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)." + help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests." }, completed_at: { label: "完了日時" @@ -315,7 +315,7 @@ export const jaJPObjects: NonNullable = { sys_approval_delegation: { label: "Approval Delegation", pluralLabel: "Approval Delegations", - description: "Self-service out-of-office rule: route this user's approver slots to a delegate within a time window.", + description: "While someone is away, approvals assigned to them personally go to their delegate.", fields: { id: { label: "Delegation ID" @@ -334,7 +334,7 @@ export const jaJPObjects: NonNullable = { }, valid_from: { label: "Valid From", - help: "Rule is inactive before this instant. Null = active immediately. Enforced at resolution time via isGrantActive (ADR-0091 D2 predicate) — never by a background job." + help: "Rule is inactive before this instant. Null = active immediately. Enforced at resolution time via isGrantActive — never by a background job." }, valid_until: { label: "Valid Until", diff --git a/packages/plugins/plugin-approvals/src/translations/ja-JP.source-hashes.generated.ts b/packages/plugins/plugin-approvals/src/translations/ja-JP.source-hashes.generated.ts index 0b5ccda9bf4..782f325ca99 100644 --- a/packages/plugins/plugin-approvals/src/translations/ja-JP.source-hashes.generated.ts +++ b/packages/plugins/plugin-approvals/src/translations/ja-JP.source-hashes.generated.ts @@ -21,7 +21,7 @@ export const jaJPGeneratedSourceHashes: Readonly> = { "objects.sys_approval_delegation._views.active.emptyState.message": "1095d61c017b6db2", "objects.sys_approval_delegation._views.active.emptyState.title": "799d28e51d88963e", "objects.sys_approval_delegation._views.active.label": "eee107cf4e466779", - "objects.sys_approval_delegation.description": "517790acf02a372d", + "objects.sys_approval_delegation.description": "177cd798e5944c83", "objects.sys_approval_delegation.fields.created_at.label": "1f02d416befb595b", "objects.sys_approval_delegation.fields.delegate_id.help": "9cfc641f7b0b04f9", "objects.sys_approval_delegation.fields.delegate_id.label": "afd6d8733dc5bc14", @@ -35,16 +35,16 @@ export const jaJPGeneratedSourceHashes: Readonly> = { "objects.sys_approval_delegation.fields.reason.help": "712516f1461dff2b", "objects.sys_approval_delegation.fields.reason.label": "ac399f03d7f2e64d", "objects.sys_approval_delegation.fields.updated_at.label": "aba63dc2a9c79b8d", - "objects.sys_approval_delegation.fields.valid_from.help": "01386010c454bfbd", + "objects.sys_approval_delegation.fields.valid_from.help": "20b8f73a1c3976d6", "objects.sys_approval_delegation.fields.valid_from.label": "02eaf201e57c9eda", "objects.sys_approval_delegation.fields.valid_until.help": "a48ea34e65933e9d", "objects.sys_approval_delegation.fields.valid_until.label": "a68db5bd4fc72a13", "objects.sys_approval_delegation.label": "4389c4fe5f5c9746", "objects.sys_approval_delegation.pluralLabel": "f07fec3ffd74e6db", - "objects.sys_approval_request.fields.flow_node_id.help": "154aa23b4eee4cae", + "objects.sys_approval_request.fields.flow_node_id.help": "97e7489e6549d78c", "objects.sys_approval_request.fields.flow_node_id.label": "052ad568aa41227c", - "objects.sys_approval_request.fields.flow_run_id.help": "35c92818f5e11090", + "objects.sys_approval_request.fields.flow_run_id.help": "5718cf2ce8a0abd6", "objects.sys_approval_request.fields.flow_run_id.label": "d0124e323da966f9", - "objects.sys_approval_request.fields.node_config_json.help": "7ee00ef9b3ca7e80", + "objects.sys_approval_request.fields.node_config_json.help": "e832e60ef12791e6", "objects.sys_approval_request.fields.node_config_json.label": "e64eb4bf7d7ea033", }; diff --git a/packages/plugins/plugin-approvals/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-approvals/src/translations/zh-CN.objects.generated.ts index 8c347046464..6bc54fe2dc3 100644 --- a/packages/plugins/plugin-approvals/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-approvals/src/translations/zh-CN.objects.generated.ts @@ -83,15 +83,15 @@ export const zhCNObjects: NonNullable = { }, flow_run_id: { label: "Flow Run", - help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it." + help: "Suspended automation run id this request gates. The decision resumes it." }, flow_node_id: { label: "Flow Node", - help: "Approval node id within the flow that opened this request (ADR-0019)." + help: "Approval node id within the flow that opened this request." }, node_config_json: { label: "Node Config", - help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)." + help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests." }, completed_at: { label: "完成时间" @@ -315,7 +315,7 @@ export const zhCNObjects: NonNullable = { sys_approval_delegation: { label: "审批委派", pluralLabel: "审批委派", - description: "自助不在岗规则:在时间窗内把该用户的审批人槽位改派给候补人。", + description: "某人不在岗期间,直接指派给其本人的审批会转给其委派人。", fields: { id: { label: "委派 ID" @@ -334,7 +334,7 @@ export const zhCNObjects: NonNullable = { }, valid_from: { label: "生效时间", - help: "此刻之前规则不生效。留空 = 立即生效。在解析时通过 isGrantActive 强制(ADR-0091 D2 判定式)——绝不依赖后台任务。" + help: "此刻之前规则不生效。留空 = 立即生效。在解析时通过 isGrantActive 强制——绝不依赖后台任务。" }, valid_until: { label: "失效时间", diff --git a/packages/plugins/plugin-approvals/src/translations/zh-CN.source-hashes.generated.ts b/packages/plugins/plugin-approvals/src/translations/zh-CN.source-hashes.generated.ts index ae90435892a..937763db2aa 100644 --- a/packages/plugins/plugin-approvals/src/translations/zh-CN.source-hashes.generated.ts +++ b/packages/plugins/plugin-approvals/src/translations/zh-CN.source-hashes.generated.ts @@ -20,10 +20,10 @@ export const zhCNGeneratedSourceHashes: Readonly> = { "objects.sys_approval_delegation.fields.display_title.help": "2831a1ffde72b425", "objects.sys_approval_delegation.fields.display_title.label": "70f7aadecce647a5", - "objects.sys_approval_request.fields.flow_node_id.help": "154aa23b4eee4cae", + "objects.sys_approval_request.fields.flow_node_id.help": "97e7489e6549d78c", "objects.sys_approval_request.fields.flow_node_id.label": "052ad568aa41227c", - "objects.sys_approval_request.fields.flow_run_id.help": "35c92818f5e11090", + "objects.sys_approval_request.fields.flow_run_id.help": "5718cf2ce8a0abd6", "objects.sys_approval_request.fields.flow_run_id.label": "d0124e323da966f9", - "objects.sys_approval_request.fields.node_config_json.help": "7ee00ef9b3ca7e80", + "objects.sys_approval_request.fields.node_config_json.help": "e832e60ef12791e6", "objects.sys_approval_request.fields.node_config_json.label": "e64eb4bf7d7ea033", }; diff --git a/packages/plugins/plugin-security/src/objects/admin-copy-vocabulary.test.ts b/packages/plugins/plugin-security/src/objects/admin-copy-vocabulary.test.ts new file mode 100644 index 00000000000..2a1c6e96be3 --- /dev/null +++ b/packages/plugins/plugin-security/src/objects/admin-copy-vocabulary.test.ts @@ -0,0 +1,129 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Admin-facing copy of the security objects and the shipped permission sets + * carries no design-record vocabulary. + * + * Setup reaches these through Access Control and the user record page: the + * Permission Sets page lists each shipped set with its `description`, and a + * user's Positions and Permission Sets lists open the `sys_user_position` and + * `sys_user_permission_set` forms, where a field's help tooltip is the field's + * `description`. The generated bundles carry the object copy 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 and permission set this package ships is judged, not only the ones + * Setup opens today. + * + * The same rule over the other Setup producers lives in each of them + * (`@objectstack/platform-objects`' `src/apps/setup-copy-vocabulary.test.ts`, + * `@objectstack/service-messaging`'s `src/objects/admin-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 SecurityObjects from './index.js'; +import { defaultPermissionSets } from './default-permission-sets.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('security objects and permission sets: admin-facing copy carries no design-record vocabulary', () => { + it('the matcher recognises each kind of vocabulary it exists to keep out', () => { + expect(vocabularyIn('Assigns a position (ADR-0057).')).toEqual(['an ADR number']); + expect(vocabularyIn('Enforced fail-closed at resolution time (D2).')).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 positions each user holds, one row per assignment.')).toEqual([]); + }); + + it('object, field and view copy of every object this package exports', () => { + const objects: ObjectLike[] = (Object.values(SecurityObjects) as unknown[]).filter(isObjectSchema); + const names = objects.map((o) => o.name); + for (const name of ['sys_user_permission_set', 'sys_user_position', 'sys_audience_binding_suggestion']) { + 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('the label and description of every permission set this package ships', () => { + const names = defaultPermissionSets.map((ps) => ps.name); + for (const name of ['organization_admin', 'organization_admin_no_bypass', 'mcp_agent_data_read', 'mcp_agent_data_write']) { + expect(names, `${name} is in the population`).toContain(name); + } + const strings: Array<[string, string]> = []; + for (const ps of defaultPermissionSets) { + if (typeof ps.label === 'string') strings.push([`${ps.name}.label`, ps.label]); + if (typeof ps.description === 'string') strings.push([`${ps.name}.description`, ps.description]); + } + 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(50); + expect(offenders(strings)).toEqual([]); + }); +}); diff --git a/packages/plugins/plugin-security/src/objects/default-permission-sets.ts b/packages/plugins/plugin-security/src/objects/default-permission-sets.ts index 26a158db074..4c5dd9a3037 100644 --- a/packages/plugins/plugin-security/src/objects/default-permission-sets.ts +++ b/packages/plugins/plugin-security/src/objects/default-permission-sets.ts @@ -1429,7 +1429,7 @@ const baseDefaultPermissionSets: PermissionSet[] = [ label: 'MCP Agent — Read Only', description: 'Read-only ceiling for an AI agent acting on behalf of a user (OAuth `data:read`). ' + - 'Bounded by the delegating user via the ADR-0090 D10 intersection.', + 'Bounded by the delegating user\'s own permissions.', objects: { '*': { allowRead: true }, }, @@ -1439,7 +1439,7 @@ const baseDefaultPermissionSets: PermissionSet[] = [ label: 'MCP Agent — Read & Write', description: 'Read+write ceiling for an AI agent acting on behalf of a user (OAuth `data:write`). ' + - 'Full CRUD, still bounded by the delegating user via the ADR-0090 D10 intersection. ' + + 'Full CRUD, still bounded by the delegating user\'s own permissions. ' + 'Identity tables stay read-only (better-auth managed).', objects: { '*': { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true }, @@ -1484,6 +1484,8 @@ const baseDefaultPermissionSets: PermissionSet[] = [ * not (grant this). */ function deriveWallLessOrgAdmin(base: PermissionSet): PermissionSet { + // [ADR-0105 D4 / finding F2] The description is admin copy on Setup's + // Permission Sets page, so the decision ids stay here, not in the string. const objects: Record = { ...(base.objects ?? {}) }; const { viewAllRecords: _v, modifyAllRecords: _m, ...wildcardWithoutBypass } = (objects['*'] ?? {}) as Record; @@ -1495,7 +1497,7 @@ function deriveWallLessOrgAdmin(base: PermissionSet): PermissionSet { description: 'Organization administration WITHOUT blanket record visibility. Granted instead of ' + '`organization_admin` when the tenancy posture enforces no organization wall, where nothing ' + - 'would bound the superuser bits (ADR-0105 D4 / finding F2). Ownership, sharing and business ' + + 'would bound the superuser bits. Ownership, sharing and business ' + 'RLS still apply; grant `admin_full_access` (or an explicit set carrying the bits) when a ' + 'deployment-wide data superuser is genuinely intended.', objects, diff --git a/packages/plugins/plugin-security/src/objects/reserved-identity-names.ts b/packages/plugins/plugin-security/src/objects/reserved-identity-names.ts index 3054f834940..4a90266cd94 100644 --- a/packages/plugins/plugin-security/src/objects/reserved-identity-names.ts +++ b/packages/plugins/plugin-security/src/objects/reserved-identity-names.ts @@ -106,11 +106,15 @@ export function reservedIdentityNamesCelList(): string { * It names the remedy, because the refusal is not "you lack permission" — no * caller has permission, tenant admins included. The name is reserved by the * framework; a different name is the only way through. + * + * The decision record (ADR-0068 D2) is named here, not in the sentence: the + * sentence is admin copy, shown on Setup's assignment form and carried into + * every locale's bundle. */ export function reservedIdentityNameMessage(column: string): string { return ( `'${column}' cannot spell a framework-reserved built-in identity name ` + - `(${RESERVED_IDENTITY_NAMES.join(', ')}). These names are ADR-0068 built-in identities: ` + + `(${RESERVED_IDENTITY_NAMES.join(', ')}). These names are built-in identities: ` + `the platform projects them into current_user.positions from their own sources of truth, ` + `and a row spelling one is not an assignment of that identity. Choose a different name.` ); diff --git a/packages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts b/packages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts index c61d0bdc52c..a85ad1f5c3f 100644 --- a/packages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts @@ -36,7 +36,9 @@ export const SysAudienceBindingSuggestion = ObjectSchema.create({ icon: 'shield-question', isSystem: true, managedBy: 'engine-owned', - description: 'Package-suggested everyone-anchor grant and the deployment-level decision on it (ADR-0090 D5/D9).', + // [ADR-0090 D5/D9] The everyone-anchor suggestion and its deployment-level + // decision, in the words Setup shows. + description: 'A package\'s suggestion to give one of its permission sets to everyone who signs in, and the decision taken on it.', titleFormat: '{package_id}: {permission_set_name} → {anchor}', highlightFields: ['package_id', 'permission_set_name', 'anchor', 'status'], @@ -79,7 +81,8 @@ export const SysAudienceBindingSuggestion = ObjectSchema.create({ // real reverse-domain package id is 57 characters — the floor is cleared // with room to spare. maxLength: 255, - description: 'Owning package that ships the suggested permission set (ADR-0086 D3 provenance).', + // [ADR-0086 D3] Package provenance. + description: 'Owning package that ships the suggested permission set.', }), permission_set_name: Field.text({ @@ -124,7 +127,8 @@ export const SysAudienceBindingSuggestion = ObjectSchema.create({ required: true, readonly: true, defaultValue: 'everyone', - description: 'Audience anchor position the package suggests binding to (ADR-0090 D9).', + // [ADR-0090 D9] The audience anchors. + description: 'Audience anchor position the package suggests binding to.', options: [ { value: 'everyone', label: 'Everyone' }, { value: 'guest', label: 'Guest' }, diff --git a/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts index 4405da69e60..b13bb2de077 100644 --- a/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts @@ -96,7 +96,7 @@ export const SysUserPermissionSet = ObjectSchema.create({ // a text input where the widget is not registered. widget: 'permission-ref', description: - '[ADR-0131 D4] Machine name of the permission set this grant holds, as the security catalog names it. ' + + 'Machine name of the permission set this grant holds, as the security catalog names it. ' + 'A name the catalog does not hold is refused. NULL only on a grant written before the column existed.', }), @@ -121,30 +121,33 @@ export const SysUserPermissionSet = ObjectSchema.create({ description: 'User who granted this permission set.', }), + // [ADR-0091 D1; enforced by the D2 predicate] valid_from: Field.datetime({ label: 'Valid From', required: false, description: - '[ADR-0091 D1] Grant is inactive before this instant. Null = active immediately. ' + - 'Enforced fail-closed at resolution time (D2) — never by a background job.', + 'Grant is inactive before this instant. Null = active immediately. ' + + 'Enforced fail-closed at resolution time — never by a background job.', }), + // [ADR-0091 D1; mandatory on D4 break-glass and D6 agent grants; enforced by the D2 predicate] valid_until: Field.datetime({ label: 'Valid Until', required: false, description: - '[ADR-0091 D1] Grant is inactive AT and AFTER this instant (half-open [from, until), UTC). ' + - 'Null = never expires. Mandatory on break-glass activations (D4) and agent grants (D6). ' + - 'Enforced at resolution time (D2).', + 'Grant is inactive AT and AFTER this instant (half-open [from, until), UTC). ' + + 'Null = never expires. Mandatory on break-glass activations and agent grants. ' + + 'Enforced at resolution time.', }), reason: Field.text({ label: 'Reason', required: false, maxLength: 500, + // [ADR-0091 D1; required on D3 delegation and D4 break-glass rows; D6 agent grants] description: - '[ADR-0091 D1] Why this grant exists. Free text; REQUIRED on delegation (D3) and break-glass (D4) rows. ' + - 'Agent grants carry the task/run attribution here (D6).', + 'Why this grant exists. Free text; REQUIRED on delegation and break-glass rows. ' + + 'Agent grants carry the task/run attribution here.', }), // [#9730] `delegated_from` was RETIRED from this object (maintainer ruling @@ -191,11 +194,12 @@ export const SysUserPermissionSet = ObjectSchema.create({ // than merely omitting the promise: a reader who remembers the old wording // has to be told it was wrong, not left to infer it. If D5 is ever // implemented, these two descriptions are what must change with it. + // [ADR-0091 D5] The recertification pair. last_certified_at: Field.datetime({ label: 'Last Certified At', required: false, description: - '[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. ' + + 'Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. ' + 'Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives ' + '"never certified" or "certification stale" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed.', }), @@ -204,7 +208,7 @@ export const SysUserPermissionSet = ObjectSchema.create({ label: 'Certified By', required: false, description: - '[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. ' + + 'Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. ' + 'Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — ' + 'the platform checks nothing about it and grants nothing on the strength of it.', }), @@ -249,8 +253,9 @@ export const SysUserPermissionSet = ObjectSchema.create({ type: 'cross_field', name: 'validity_window_order', label: 'Validity window ends after it starts', + // [ADR-0091 D1/D2] The half-open validity window. description: - '[ADR-0091 D1/D2] A grant is active in the half-open window [valid_from, valid_until), so a window that ' + 'A grant is active in the half-open window [valid_from, valid_until), so a window that ' + 'ends at or before it starts grants nothing. Judged on insert, and on an update that moves a bound.', fields: ['valid_until', 'valid_from'], condition: P`(previous == null || record.valid_from != previous.valid_from || record.valid_until != previous.valid_until) && record.valid_from != null && record.valid_until != null && datetime(record.valid_until) <= datetime(record.valid_from)`, diff --git a/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts b/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts index 7d2cb6169a6..fbd17436adf 100644 --- a/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts @@ -41,7 +41,9 @@ export const SysUserPosition = ObjectSchema.create({ // default is full CRUD, so no `userActions` block is needed — the affordance is // a declaration only; the DelegatedAdminGate is the authz. managedBy: 'system-data', - description: 'Assigns a position, by name, to a user. Platform-owned (ADR-0057 D4, ADR-0090 D3).', + // [ADR-0057 D4, ADR-0090 D3] The platform-owned assignment record, in the + // words Setup shows. + description: 'Which positions each user holds, one row per assignment.', // [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 @@ -92,12 +94,13 @@ export const SysUserPosition = ObjectSchema.create({ description: 'Position machine name, resolved in the security catalog.', }), + // [ADR-0090 Addendum; the delegated-admin boundary is ADR-0090 D12] business_unit_id: Field.lookup('sys_business_unit', { label: 'Business Unit', required: false, description: - '[ADR-0090 Addendum] Assignment-level BU anchor: where this position assignment applies. ' + - 'Depth anchor for readScope/writeScope, delegated-admin boundary (D12), and audit fact. ' + + 'Assignment-level BU anchor: where this position assignment applies. ' + + 'Depth anchor for readScope/writeScope, delegated-admin boundary, and audit fact. ' + 'Null = unanchored (legacy/tenant-wide); delegated admins MUST anchor assignments inside their subtree.', }), @@ -121,35 +124,38 @@ export const SysUserPosition = ObjectSchema.create({ description: 'User who granted this position assignment (stamped by the delegated-admin gate for delegate writes).', }), + // [ADR-0091 D1; enforced by the D2 predicate] valid_from: Field.datetime({ label: 'Valid From', required: false, description: - '[ADR-0091 D1] Grant is inactive before this instant. Null = active immediately. ' + - 'Enforced fail-closed at resolution time (D2) — never by a background job.', + 'Grant is inactive before this instant. Null = active immediately. ' + + 'Enforced fail-closed at resolution time — never by a background job.', }), + // [ADR-0091 D1; mandatory on D3 delegation rows; enforced by the D2 predicate] valid_until: Field.datetime({ label: 'Valid Until', required: false, description: - '[ADR-0091 D1] Grant is inactive AT and AFTER this instant (half-open [from, until), UTC). ' + - 'Null = never expires. Mandatory on delegation rows (D3). Enforced at resolution time (D2).', + 'Grant is inactive AT and AFTER this instant (half-open [from, until), UTC). ' + + 'Null = never expires. Mandatory on delegation rows. Enforced at resolution time.', }), reason: Field.text({ label: 'Reason', required: false, maxLength: 500, - description: - '[ADR-0091 D1] Why this grant exists. Free text; REQUIRED on delegation (D3) and break-glass (D4) rows.', + // [ADR-0091 D1; required on D3 delegation and D4 break-glass rows] + description: 'Why this grant exists. Free text; REQUIRED on delegation and break-glass rows.', }), + // [ADR-0091 D3] Delegation of duty. delegated_from: Field.lookup('sys_user', { label: 'Delegated From', required: false, description: - '[ADR-0091 D3] The delegator whose authority this row carries (职务代理). ' + + 'The delegator whose authority this row carries (职务代理). ' + 'A row with delegated_from set is not itself delegatable and not self-renewable — chains are cut both ways.', }), @@ -167,11 +173,12 @@ export const SysUserPosition = ObjectSchema.create({ // ruling, 2026-08-13): the claim is withdrawn in prose rather than the // workflow built or the columns dropped. Full rationale sits with the // sibling declaration in sys-user-permission-set.object.ts. + // [ADR-0091 D5] The recertification pair. last_certified_at: Field.datetime({ label: 'Last Certified At', required: false, description: - '[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. ' + + 'Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. ' + 'Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives ' + '"never certified" or "certification stale" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed.', }), @@ -180,7 +187,7 @@ export const SysUserPosition = ObjectSchema.create({ label: 'Certified By', required: false, description: - '[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. ' + + 'Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. ' + 'Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — ' + 'the platform checks nothing about it and grants nothing on the strength of it.', }), @@ -248,8 +255,9 @@ export const SysUserPosition = ObjectSchema.create({ type: 'script', name: 'reserved_identity_position', label: 'Reserved built-in identity name', + // [ADR-0068 D2] The built-in identity names. description: - 'ADR-0068 D2 built-in identity names are a projection with their own sources of truth. A stored ' + 'Built-in identity names are a projection with their own sources of truth. A stored ' + 'assignment row spelling one grants nothing and misrepresents the holder, so it is refused.', condition: { dialect: 'cel', source: `record.position in ${reservedIdentityNamesCelList()}` }, severity: 'error', diff --git a/packages/plugins/plugin-security/src/translations/en.objects.generated.ts b/packages/plugins/plugin-security/src/translations/en.objects.generated.ts index 564c8050911..e62ef941a00 100644 --- a/packages/plugins/plugin-security/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/en.objects.generated.ts @@ -34,7 +34,7 @@ export const enObjects: NonNullable = { }, permission_set: { label: "Permission Set", - help: "[ADR-0131 D4] Machine name of the permission set this grant holds, as the security catalog names it. A name the catalog does not hold is refused. NULL only on a grant written before the column existed." + help: "Machine name of the permission set this grant holds, as the security catalog names it. A name the catalog does not hold is refused. NULL only on a grant written before the column existed." }, organization_id: { label: "Organization", @@ -46,23 +46,23 @@ export const enObjects: NonNullable = { }, valid_from: { label: "Valid From", - help: "[ADR-0091 D1] Grant is inactive before this instant. Null = active immediately. Enforced fail-closed at resolution time (D2) — never by a background job." + help: "Grant is inactive before this instant. Null = active immediately. Enforced fail-closed at resolution time — never by a background job." }, valid_until: { label: "Valid Until", - help: "[ADR-0091 D1] Grant is inactive AT and AFTER this instant (half-open [from, until), UTC). Null = never expires. Mandatory on break-glass activations (D4) and agent grants (D6). Enforced at resolution time (D2)." + help: "Grant is inactive AT and AFTER this instant (half-open [from, until), UTC). Null = never expires. Mandatory on break-glass activations and agent grants. Enforced at resolution time." }, reason: { label: "Reason", - help: "[ADR-0091 D1] Why this grant exists. Free text; REQUIRED on delegation (D3) and break-glass (D4) rows. Agent grants carry the task/run attribution here (D6)." + help: "Why this grant exists. Free text; REQUIRED on delegation and break-glass rows. Agent grants carry the task/run attribution here." }, last_certified_at: { label: "Last Certified At", - help: "[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives \"never certified\" or \"certification stale\" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed." + help: "Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives \"never certified\" or \"certification stale\" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed." }, certified_by: { label: "Certified By", - help: "[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — the platform checks nothing about it and grants nothing on the strength of it." + help: "Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — the platform checks nothing about it and grants nothing on the strength of it." }, created_at: { label: "Created At" @@ -80,7 +80,7 @@ export const enObjects: NonNullable = { sys_user_position: { label: "User Position", pluralLabel: "User Positions", - description: "Assigns a position, by name, to a user. Platform-owned (ADR-0057 D4, ADR-0090 D3).", + description: "Which positions each user holds, one row per assignment.", fields: { id: { label: "Assignment ID", @@ -100,7 +100,7 @@ export const enObjects: NonNullable = { }, business_unit_id: { label: "Business Unit", - help: "[ADR-0090 Addendum] Assignment-level BU anchor: where this position assignment applies. Depth anchor for readScope/writeScope, delegated-admin boundary (D12), and audit fact. Null = unanchored (legacy/tenant-wide); delegated admins MUST anchor assignments inside their subtree." + help: "Assignment-level BU anchor: where this position assignment applies. Depth anchor for readScope/writeScope, delegated-admin boundary, and audit fact. Null = unanchored (legacy/tenant-wide); delegated admins MUST anchor assignments inside their subtree." }, organization_id: { label: "Organization", @@ -112,27 +112,27 @@ export const enObjects: NonNullable = { }, valid_from: { label: "Valid From", - help: "[ADR-0091 D1] Grant is inactive before this instant. Null = active immediately. Enforced fail-closed at resolution time (D2) — never by a background job." + help: "Grant is inactive before this instant. Null = active immediately. Enforced fail-closed at resolution time — never by a background job." }, valid_until: { label: "Valid Until", - help: "[ADR-0091 D1] Grant is inactive AT and AFTER this instant (half-open [from, until), UTC). Null = never expires. Mandatory on delegation rows (D3). Enforced at resolution time (D2)." + help: "Grant is inactive AT and AFTER this instant (half-open [from, until), UTC). Null = never expires. Mandatory on delegation rows. Enforced at resolution time." }, reason: { label: "Reason", - help: "[ADR-0091 D1] Why this grant exists. Free text; REQUIRED on delegation (D3) and break-glass (D4) rows." + help: "Why this grant exists. Free text; REQUIRED on delegation and break-glass rows." }, delegated_from: { label: "Delegated From", - help: "[ADR-0091 D3] The delegator whose authority this row carries (职务代理). A row with delegated_from set is not itself delegatable and not self-renewable — chains are cut both ways." + help: "The delegator whose authority this row carries (职务代理). A row with delegated_from set is not itself delegatable and not self-renewable — chains are cut both ways." }, last_certified_at: { label: "Last Certified At", - help: "[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives \"never certified\" or \"certification stale\" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed." + help: "Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives \"never certified\" or \"certification stale\" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed." }, certified_by: { label: "Certified By", - help: "[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — the platform checks nothing about it and grants nothing on the strength of it." + help: "Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — the platform checks nothing about it and grants nothing on the strength of it." }, created_at: { label: "Created At" @@ -143,7 +143,7 @@ export const enObjects: NonNullable = { }, _validations: { reserved_identity_position: { - message: "'position' cannot spell a framework-reserved built-in identity name (platform_admin, org_owner, org_admin, org_member). These names are ADR-0068 built-in identities: the platform projects them into current_user.positions from their own sources of truth, and a row spelling one is not an assignment of that identity. Choose a different name." + message: "'position' cannot spell a framework-reserved built-in identity name (platform_admin, org_owner, org_admin, org_member). These names are built-in identities: the platform projects them into current_user.positions from their own sources of truth, and a row spelling one is not an assignment of that identity. Choose a different name." } } } diff --git a/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts index e99fd4e3551..708ead1d327 100644 --- a/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts @@ -34,7 +34,7 @@ export const esESObjects: NonNullable = { }, permission_set: { label: "Conjunto de permisos", - help: "[ADR-0131 D4] Nombre de máquina del conjunto de permisos que otorga esta asignación, tal como lo nombra el catálogo de seguridad. Un nombre que el catálogo no contiene se rechaza. NULL solo en una asignación escrita antes de que existiera la columna." + help: "Nombre de máquina del conjunto de permisos que otorga esta asignación, tal como lo nombra el catálogo de seguridad. Un nombre que el catálogo no contiene se rechaza. NULL solo en una asignación escrita antes de que existiera la columna." }, organization_id: { label: "Organización", @@ -46,23 +46,23 @@ export const esESObjects: NonNullable = { }, valid_from: { label: "Válido desde", - help: "[ADR-0091 D1] La concesión está inactiva antes de este instante. Nulo = activa de inmediato. Se aplica en modo fail-closed en el momento de resolución (D2), nunca mediante un trabajo en segundo plano." + help: "La concesión está inactiva antes de este instante. Nulo = activa de inmediato. Se aplica en modo fail-closed en el momento de resolución, nunca mediante un trabajo en segundo plano." }, valid_until: { label: "Válido hasta", - help: "[ADR-0091 D1] La concesión queda inactiva DESDE este instante inclusive (intervalo semiabierto [from, until), UTC). Null = nunca expira. Obligatorio en activaciones de emergencia (D4) y concesiones de agente (D6). Se aplica en el momento de la resolución (D2)." + help: "La concesión queda inactiva DESDE este instante inclusive (intervalo semiabierto [from, until), UTC). Null = nunca expira. Obligatorio en activaciones de emergencia y concesiones de agente. Se aplica en el momento de la resolución." }, reason: { label: "Motivo", - help: "[ADR-0091 D1] Por qué existe esta concesión. Texto libre; OBLIGATORIO en filas de delegación (D3) y de acceso de emergencia (D4). Las concesiones de agente registran aquí la atribución de tarea/ejecución (D6)." + help: "Por qué existe esta concesión. Texto libre; OBLIGATORIO en filas de delegación y de acceso de emergencia. Las concesiones de agente registran aquí la atribución de tarea/ejecución." }, last_certified_at: { label: "Última certificación", - help: "[ADR-0091 D5] Reservada para un futuro flujo de recertificación de accesos, que registraría aquí cuándo se atestiguó por última vez esta concesión. Hoy es inerte: ningún código de la plataforma escribe esta columna ni la lee; ninguna ruta de resolución, control ni regla de lint la consulta, y nada deriva de ella «nunca certificada» ni «certificación obsoleta». Por tanto, un valor nulo significa que el flujo no existe, no que esta concesión no se haya revisado." + help: "Reservada para un futuro flujo de recertificación de accesos, que registraría aquí cuándo se atestiguó por última vez esta concesión. Hoy es inerte: ningún código de la plataforma escribe esta columna ni la lee; ninguna ruta de resolución, control ni regla de lint la consulta, y nada deriva de ella «nunca certificada» ni «certificación obsoleta». Por tanto, un valor nulo significa que el flujo no existe, no que esta concesión no se haya revisado." }, certified_by: { label: "Certificado por", - help: "[ADR-0091 D5] Reservada para ese mismo futuro flujo de recertificación de accesos: el revisor que atestiguaría esta concesión. Hoy es inerte: ningún código de la plataforma la escribe ni la lee. Un valor escrito aquí por un cliente es una anotación sin verificar; la plataforma no comprueba nada sobre él ni concede nada en función de él." + help: "Reservada para ese mismo futuro flujo de recertificación de accesos: el revisor que atestiguaría esta concesión. Hoy es inerte: ningún código de la plataforma la escribe ni la lee. Un valor escrito aquí por un cliente es una anotación sin verificar; la plataforma no comprueba nada sobre él ni concede nada en función de él." }, created_at: { label: "Creado el" @@ -80,7 +80,7 @@ export const esESObjects: NonNullable = { sys_user_position: { label: "Puesto de usuario", pluralLabel: "Puestos de usuario", - description: "Asigna un puesto, por nombre, a un usuario. Gestionado por la plataforma (ADR-0057 D4, ADR-0090 D3).", + description: "Qué puestos ocupa cada usuario, una fila por asignación.", fields: { id: { label: "ID de asignación", @@ -100,7 +100,7 @@ export const esESObjects: NonNullable = { }, business_unit_id: { label: "Unidad de negocio", - help: "[Anexo ADR-0090] Ancla de UN a nivel de asignación: dónde se aplica esta asignación de puesto. Ancla de profundidad para readScope/writeScope, límite de administración delegada (D12) y hecho de auditoría. Nulo = sin anclar (heredado/todo el inquilino); los administradores delegados DEBEN anclar las asignaciones dentro de su subárbol." + help: "Ancla de UN a nivel de asignación: dónde se aplica esta asignación de puesto. Ancla de profundidad para readScope/writeScope, límite de administración delegada y hecho de auditoría. Nulo = sin anclar (heredado/todo el inquilino); los administradores delegados DEBEN anclar las asignaciones dentro de su subárbol." }, organization_id: { label: "Organización", @@ -112,27 +112,27 @@ export const esESObjects: NonNullable = { }, valid_from: { label: "Válido desde", - help: "[ADR-0091 D1] La concesión está inactiva antes de este instante. Nulo = activa de inmediato. Se aplica en modo fail-closed en el momento de resolución (D2), nunca mediante un trabajo en segundo plano." + help: "La concesión está inactiva antes de este instante. Nulo = activa de inmediato. Se aplica en modo fail-closed en el momento de resolución, nunca mediante un trabajo en segundo plano." }, valid_until: { label: "Válido hasta", - help: "[ADR-0091 D1] La concesión está inactiva EN y DESPUÉS de este instante (intervalo semiabierto [from, until), UTC). Nulo = nunca expira. Obligatorio en filas de delegación (D3). Se aplica en el momento de resolución (D2)." + help: "La concesión está inactiva EN y DESPUÉS de este instante (intervalo semiabierto [from, until), UTC). Nulo = nunca expira. Obligatorio en filas de delegación. Se aplica en el momento de resolución." }, reason: { label: "Motivo", - help: "[ADR-0091 D1] Por qué existe esta concesión. Texto libre; OBLIGATORIO en filas de delegación (D3) y de acceso de emergencia (D4)." + help: "Por qué existe esta concesión. Texto libre; OBLIGATORIO en filas de delegación y de acceso de emergencia." }, delegated_from: { label: "Delegado desde", - help: "[ADR-0091 D3] El delegador cuya autoridad porta esta fila (职务代理). Una fila con delegated_from establecido no es delegable ni autorrenovable: las cadenas se cortan en ambos sentidos." + help: "El delegador cuya autoridad porta esta fila (职务代理). Una fila con delegated_from establecido no es delegable ni autorrenovable: las cadenas se cortan en ambos sentidos." }, last_certified_at: { label: "Última certificación", - help: "[ADR-0091 D5] Reservada para un futuro flujo de recertificación de accesos, que registraría aquí cuándo se atestiguó por última vez esta concesión. Hoy es inerte: ningún código de la plataforma escribe esta columna ni la lee; ninguna ruta de resolución, control ni regla de lint la consulta, y nada deriva de ella «nunca certificada» ni «certificación obsoleta». Por tanto, un valor nulo significa que el flujo no existe, no que esta concesión no se haya revisado." + help: "Reservada para un futuro flujo de recertificación de accesos, que registraría aquí cuándo se atestiguó por última vez esta concesión. Hoy es inerte: ningún código de la plataforma escribe esta columna ni la lee; ninguna ruta de resolución, control ni regla de lint la consulta, y nada deriva de ella «nunca certificada» ni «certificación obsoleta». Por tanto, un valor nulo significa que el flujo no existe, no que esta concesión no se haya revisado." }, certified_by: { label: "Certificado por", - help: "[ADR-0091 D5] Reservada para ese mismo futuro flujo de recertificación de accesos: el revisor que atestiguaría esta concesión. Hoy es inerte: ningún código de la plataforma la escribe ni la lee. Un valor escrito aquí por un cliente es una anotación sin verificar; la plataforma no comprueba nada sobre él ni concede nada en función de él." + help: "Reservada para ese mismo futuro flujo de recertificación de accesos: el revisor que atestiguaría esta concesión. Hoy es inerte: ningún código de la plataforma la escribe ni la lee. Un valor escrito aquí por un cliente es una anotación sin verificar; la plataforma no comprueba nada sobre él ni concede nada en función de él." }, created_at: { label: "Creado el" @@ -143,7 +143,7 @@ export const esESObjects: NonNullable = { }, _validations: { reserved_identity_position: { - message: "'position' cannot spell a framework-reserved built-in identity name (platform_admin, org_owner, org_admin, org_member). These names are ADR-0068 built-in identities: the platform projects them into current_user.positions from their own sources of truth, and a row spelling one is not an assignment of that identity. Choose a different name." + message: "'position' cannot spell a framework-reserved built-in identity name (platform_admin, org_owner, org_admin, org_member). These names are built-in identities: the platform projects them into current_user.positions from their own sources of truth, and a row spelling one is not an assignment of that identity. Choose a different name." } } } diff --git a/packages/plugins/plugin-security/src/translations/es-ES.source-hashes.generated.ts b/packages/plugins/plugin-security/src/translations/es-ES.source-hashes.generated.ts index 6d5c033bc79..9df5465a374 100644 --- a/packages/plugins/plugin-security/src/translations/es-ES.source-hashes.generated.ts +++ b/packages/plugins/plugin-security/src/translations/es-ES.source-hashes.generated.ts @@ -21,7 +21,7 @@ export const esESGeneratedSourceHashes: Readonly> = { "objects.sys_user_permission_set._validations.validity_window_order.message": "b2216aacc3b6f97b", "objects.sys_user_permission_set.fields.display_title.help": "ee7ef063488cd6c3", "objects.sys_user_permission_set.fields.display_title.label": "70f7aadecce647a5", - "objects.sys_user_position._validations.reserved_identity_position.message": "f35df6c1ef1493b0", + "objects.sys_user_position._validations.reserved_identity_position.message": "5cb33eea311d7306", "objects.sys_user_position.fields.display_title.help": "39b5571b48e7d800", "objects.sys_user_position.fields.display_title.label": "70f7aadecce647a5", }; diff --git a/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts index 30adca29967..d3f95540cd3 100644 --- a/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts @@ -34,7 +34,7 @@ export const jaJPObjects: NonNullable = { }, permission_set: { label: "権限セット", - help: "[ADR-0131 D4] この付与が保持する権限セットのマシン名(セキュリティカタログでの名前)。カタログにない名前は拒否されます。この列が存在する前に書き込まれた付与でのみ NULL です。" + help: "この付与が保持する権限セットのマシン名(セキュリティカタログでの名前)。カタログにない名前は拒否されます。この列が存在する前に書き込まれた付与でのみ NULL です。" }, organization_id: { label: "組織", @@ -46,23 +46,23 @@ export const jaJPObjects: NonNullable = { }, valid_from: { label: "有効開始", - help: "[ADR-0091 D1] この時点より前は付与が無効です。Null = 即時有効。解決時にフェイルクローズで適用されます(D2)——バックグラウンドジョブでは行いません。" + help: "この時点より前は付与が無効です。Null = 即時有効。解決時にフェイルクローズで適用されます——バックグラウンドジョブでは行いません。" }, valid_until: { label: "有効終了", - help: "[ADR-0091 D1] この時点以降(当該時点を含む)、付与は無効になります(半開区間 [from, until)、UTC)。null は無期限。ブレークグラス発動(D4)とエージェント付与(D6)では必須。解決時に強制されます(D2)。" + help: "この時点以降(当該時点を含む)、付与は無効になります(半開区間 [from, until)、UTC)。null は無期限。ブレークグラス発動とエージェント付与では必須。解決時に強制されます。" }, reason: { label: "理由", - help: "[ADR-0091 D1] この付与が存在する理由。自由記述。委任(D3)とブレークグラス(D4)の行では必須。エージェント付与はここにタスク/実行の帰属を記録します(D6)。" + help: "この付与が存在する理由。自由記述。委任とブレークグラスの行では必須。エージェント付与はここにタスク/実行の帰属を記録します。" }, last_certified_at: { label: "最終認証日時", - help: "[ADR-0091 D5] 将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与が最後に証明された日時を記録します。現時点では不活性です。プラットフォームはこの列に書き込まず、読み取るコードもありません。解決経路もゲートも lint も参照せず、「未認証」や「認証が古い」を導出する箇所もありません。したがって Null はワークフローが存在しないことを意味するだけで、この付与が未レビューであることを意味しません。" + help: "将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与が最後に証明された日時を記録します。現時点では不活性です。プラットフォームはこの列に書き込まず、読み取るコードもありません。解決経路もゲートも lint も参照せず、「未認証」や「認証が古い」を導出する箇所もありません。したがって Null はワークフローが存在しないことを意味するだけで、この付与が未レビューであることを意味しません。" }, certified_by: { label: "認証者", - help: "[ADR-0091 D5] 同じく将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与を証明したレビュアーを記録します。現時点では不活性で、プラットフォームは書き込みも読み取りも行いません。クライアントがここに書き込んだ値は検証されない注記にすぎず、プラットフォームはそれを検証せず、それを根拠に権限を与えることもありません。" + help: "同じく将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与を証明したレビュアーを記録します。現時点では不活性で、プラットフォームは書き込みも読み取りも行いません。クライアントがここに書き込んだ値は検証されない注記にすぎず、プラットフォームはそれを検証せず、それを根拠に権限を与えることもありません。" }, created_at: { label: "作成日時" @@ -80,7 +80,7 @@ export const jaJPObjects: NonNullable = { sys_user_position: { label: "ユーザーポジション", pluralLabel: "ユーザーポジション", - description: "ポジションを名前でユーザーに割り当てます。プラットフォーム管理(ADR-0057 D4、ADR-0090 D3)。", + description: "各ユーザーが担当するポジション。割り当てごとに 1 行です。", fields: { id: { label: "割り当て ID", @@ -100,7 +100,7 @@ export const jaJPObjects: NonNullable = { }, business_unit_id: { label: "ビジネスユニット", - help: "[ADR-0090 補遺] 割り当てレベルの BU アンカー:このポジション割り当てが適用される範囲。readScope/writeScope の深さアンカー、委任管理の境界(D12)、監査事実として機能します。Null = アンカーなし(レガシー/テナント全体)。委任管理者は割り当てを自身のサブツリー内にアンカーする必要があります。" + help: "割り当てレベルの BU アンカー:このポジション割り当てが適用される範囲。readScope/writeScope の深さアンカー、委任管理の境界、監査事実として機能します。Null = アンカーなし(レガシー/テナント全体)。委任管理者は割り当てを自身のサブツリー内にアンカーする必要があります。" }, organization_id: { label: "組織", @@ -112,27 +112,27 @@ export const jaJPObjects: NonNullable = { }, valid_from: { label: "有効開始", - help: "[ADR-0091 D1] この時点より前は付与が無効です。Null = 即時有効。解決時にフェイルクローズで適用されます(D2)——バックグラウンドジョブでは行いません。" + help: "この時点より前は付与が無効です。Null = 即時有効。解決時にフェイルクローズで適用されます——バックグラウンドジョブでは行いません。" }, valid_until: { label: "有効終了", - help: "[ADR-0091 D1] この時点以降は付与が無効です(半開区間 [from, until)、UTC)。Null = 無期限。委任行では必須(D3)。解決時に適用されます(D2)。" + help: "この時点以降は付与が無効です(半開区間 [from, until)、UTC)。Null = 無期限。委任行では必須。解決時に適用されます。" }, reason: { label: "理由", - help: "[ADR-0091 D1] この付与が存在する理由。自由記述。委任(D3)およびブレークグラス(D4)の行では必須。" + help: "この付与が存在する理由。自由記述。委任およびブレークグラスの行では必須。" }, delegated_from: { label: "委任元", - help: "[ADR-0091 D3] この行が担う権限の委任者(職務代理)。delegated_from が設定された行は、それ自体を再委任できず、自己更新もできません——連鎖は双方向で断ち切られます。" + help: "この行が担う権限の委任者(職務代理)。delegated_from が設定された行は、それ自体を再委任できず、自己更新もできません——連鎖は双方向で断ち切られます。" }, last_certified_at: { label: "最終認証日時", - help: "[ADR-0091 D5] 将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与が最後に証明された日時を記録します。現時点では不活性です。プラットフォームはこの列に書き込まず、読み取るコードもありません。解決経路もゲートも lint も参照せず、「未認証」や「認証が古い」を導出する箇所もありません。したがって Null はワークフローが存在しないことを意味するだけで、この付与が未レビューであることを意味しません。" + help: "将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与が最後に証明された日時を記録します。現時点では不活性です。プラットフォームはこの列に書き込まず、読み取るコードもありません。解決経路もゲートも lint も参照せず、「未認証」や「認証が古い」を導出する箇所もありません。したがって Null はワークフローが存在しないことを意味するだけで、この付与が未レビューであることを意味しません。" }, certified_by: { label: "認証者", - help: "[ADR-0091 D5] 同じく将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与を証明したレビュアーを記録します。現時点では不活性で、プラットフォームは書き込みも読み取りも行いません。クライアントがここに書き込んだ値は検証されない注記にすぎず、プラットフォームはそれを検証せず、それを根拠に権限を与えることもありません。" + help: "同じく将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与を証明したレビュアーを記録します。現時点では不活性で、プラットフォームは書き込みも読み取りも行いません。クライアントがここに書き込んだ値は検証されない注記にすぎず、プラットフォームはそれを検証せず、それを根拠に権限を与えることもありません。" }, created_at: { label: "作成日時" @@ -143,7 +143,7 @@ export const jaJPObjects: NonNullable = { }, _validations: { reserved_identity_position: { - message: "'position' cannot spell a framework-reserved built-in identity name (platform_admin, org_owner, org_admin, org_member). These names are ADR-0068 built-in identities: the platform projects them into current_user.positions from their own sources of truth, and a row spelling one is not an assignment of that identity. Choose a different name." + message: "'position' cannot spell a framework-reserved built-in identity name (platform_admin, org_owner, org_admin, org_member). These names are built-in identities: the platform projects them into current_user.positions from their own sources of truth, and a row spelling one is not an assignment of that identity. Choose a different name." } } } diff --git a/packages/plugins/plugin-security/src/translations/ja-JP.source-hashes.generated.ts b/packages/plugins/plugin-security/src/translations/ja-JP.source-hashes.generated.ts index 37812fced2f..f43e55aef1d 100644 --- a/packages/plugins/plugin-security/src/translations/ja-JP.source-hashes.generated.ts +++ b/packages/plugins/plugin-security/src/translations/ja-JP.source-hashes.generated.ts @@ -21,7 +21,7 @@ export const jaJPGeneratedSourceHashes: Readonly> = { "objects.sys_user_permission_set._validations.validity_window_order.message": "b2216aacc3b6f97b", "objects.sys_user_permission_set.fields.display_title.help": "ee7ef063488cd6c3", "objects.sys_user_permission_set.fields.display_title.label": "70f7aadecce647a5", - "objects.sys_user_position._validations.reserved_identity_position.message": "f35df6c1ef1493b0", + "objects.sys_user_position._validations.reserved_identity_position.message": "5cb33eea311d7306", "objects.sys_user_position.fields.display_title.help": "39b5571b48e7d800", "objects.sys_user_position.fields.display_title.label": "70f7aadecce647a5", }; diff --git a/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts index b21476351f6..f4f213c20bc 100644 --- a/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts @@ -34,7 +34,7 @@ export const zhCNObjects: NonNullable = { }, permission_set: { label: "权限集", - help: "[ADR-0131 D4] 此授权所持有的权限集的机器名,以安全目录中的名称为准。安全目录中不存在的名称将被拒绝。仅在此列存在之前写入的授权中为 NULL。" + help: "此授权所持有的权限集的机器名,以安全目录中的名称为准。安全目录中不存在的名称将被拒绝。仅在此列存在之前写入的授权中为 NULL。" }, organization_id: { label: "组织", @@ -46,23 +46,23 @@ export const zhCNObjects: NonNullable = { }, valid_from: { label: "生效时间", - help: "[ADR-0091 D1] 在此时刻之前授予无效。为空 = 立即生效。在解析时以 fail-closed 方式强制执行(D2)——绝不依赖后台任务。" + help: "在此时刻之前授予无效。为空 = 立即生效。在解析时以 fail-closed 方式强制执行——绝不依赖后台任务。" }, valid_until: { label: "失效时间", - help: "[ADR-0091 D1] 授权自该时刻起(含)失效(半开区间 [from, until),UTC)。为空表示永不过期。紧急提权(D4)和智能体授权(D6)必填。在解析时强制执行(D2)。" + help: "授权自该时刻起(含)失效(半开区间 [from, until),UTC)。为空表示永不过期。紧急提权和智能体授权必填。在解析时强制执行。" }, reason: { label: "原因", - help: "[ADR-0091 D1] 该授权存在的原因。自由文本;委派(D3)和紧急提权(D4)记录必填。智能体授权在此记录任务/运行归属(D6)。" + help: "该授权存在的原因。自由文本;委派和紧急提权记录必填。智能体授权在此记录任务/运行归属。" }, last_certified_at: { label: "最近认证时间", - help: "[ADR-0091 D5] 为未来的访问权限重新认证(定期复核)流程预留:届时用于记录此授予最近一次被认证的时间。当前该列是惰性的:平台不会写入它,也没有任何代码读取它——没有任何解析路径、门禁或 lint 会参考它,也没有任何地方据此推导出「从未认证」或「认证已过期」。因此为空只表示该流程尚不存在,并不表示此授予未经复核。" + help: "为未来的访问权限重新认证(定期复核)流程预留:届时用于记录此授予最近一次被认证的时间。当前该列是惰性的:平台不会写入它,也没有任何代码读取它——没有任何解析路径、门禁或 lint 会参考它,也没有任何地方据此推导出「从未认证」或「认证已过期」。因此为空只表示该流程尚不存在,并不表示此授予未经复核。" }, certified_by: { label: "认证人", - help: "[ADR-0091 D5] 同样为未来的访问权限重新认证流程预留:届时用于记录认证此授予的复核人。当前该列是惰性的:平台既不写入也不读取它。客户端在此写入的值只是一条未经验证的备注——平台不会对它做任何校验,也不会据此授予任何权限。" + help: "同样为未来的访问权限重新认证流程预留:届时用于记录认证此授予的复核人。当前该列是惰性的:平台既不写入也不读取它。客户端在此写入的值只是一条未经验证的备注——平台不会对它做任何校验,也不会据此授予任何权限。" }, created_at: { label: "创建时间" @@ -80,7 +80,7 @@ export const zhCNObjects: NonNullable = { sys_user_position: { label: "用户岗位", pluralLabel: "用户岗位", - description: "按名称将岗位分配给用户。由平台管理(ADR-0057 D4、ADR-0090 D3)。", + description: "每位用户担任哪些岗位,每条分配一行。", fields: { id: { label: "分配 ID", @@ -100,7 +100,7 @@ export const zhCNObjects: NonNullable = { }, business_unit_id: { label: "业务单元", - help: "[ADR-0090 附录] 分配级 BU 锚点:此岗位分配适用的范围。作为 readScope/writeScope 的深度锚点、委派管理边界(D12)和审计事实。为空 = 未锚定(遗留/租户级);委派管理员必须将分配锚定在其子树内。" + help: "分配级 BU 锚点:此岗位分配适用的范围。作为 readScope/writeScope 的深度锚点、委派管理边界和审计事实。为空 = 未锚定(遗留/租户级);委派管理员必须将分配锚定在其子树内。" }, organization_id: { label: "组织", @@ -112,27 +112,27 @@ export const zhCNObjects: NonNullable = { }, valid_from: { label: "生效时间", - help: "[ADR-0091 D1] 在此时刻之前授予无效。为空 = 立即生效。在解析时以 fail-closed 方式强制执行(D2)——绝不依赖后台任务。" + help: "在此时刻之前授予无效。为空 = 立即生效。在解析时以 fail-closed 方式强制执行——绝不依赖后台任务。" }, valid_until: { label: "失效时间", - help: "[ADR-0091 D1] 在此时刻及之后授予无效(半开区间 [from, until),UTC)。为空 = 永不过期。委派行必填(D3)。在解析时强制执行(D2)。" + help: "在此时刻及之后授予无效(半开区间 [from, until),UTC)。为空 = 永不过期。委派行必填。在解析时强制执行。" }, reason: { label: "原因", - help: "[ADR-0091 D1] 此授予存在的原因。自由文本;委派(D3)和紧急破窗(D4)行必填。" + help: "此授予存在的原因。自由文本;委派和紧急破窗行必填。" }, delegated_from: { label: "委派自", - help: "[ADR-0091 D3] 此行所承载权限的委派人(职务代理)。设置了 delegated_from 的行本身不可再委派、不可自续期——双向切断链条。" + help: "此行所承载权限的委派人(职务代理)。设置了 delegated_from 的行本身不可再委派、不可自续期——双向切断链条。" }, last_certified_at: { label: "最近认证时间", - help: "[ADR-0091 D5] 为未来的访问权限重新认证(定期复核)流程预留:届时用于记录此授予最近一次被认证的时间。当前该列是惰性的:平台不会写入它,也没有任何代码读取它——没有任何解析路径、门禁或 lint 会参考它,也没有任何地方据此推导出「从未认证」或「认证已过期」。因此为空只表示该流程尚不存在,并不表示此授予未经复核。" + help: "为未来的访问权限重新认证(定期复核)流程预留:届时用于记录此授予最近一次被认证的时间。当前该列是惰性的:平台不会写入它,也没有任何代码读取它——没有任何解析路径、门禁或 lint 会参考它,也没有任何地方据此推导出「从未认证」或「认证已过期」。因此为空只表示该流程尚不存在,并不表示此授予未经复核。" }, certified_by: { label: "认证人", - help: "[ADR-0091 D5] 同样为未来的访问权限重新认证流程预留:届时用于记录认证此授予的复核人。当前该列是惰性的:平台既不写入也不读取它。客户端在此写入的值只是一条未经验证的备注——平台不会对它做任何校验,也不会据此授予任何权限。" + help: "同样为未来的访问权限重新认证流程预留:届时用于记录认证此授予的复核人。当前该列是惰性的:平台既不写入也不读取它。客户端在此写入的值只是一条未经验证的备注——平台不会对它做任何校验,也不会据此授予任何权限。" }, created_at: { label: "创建时间" @@ -143,7 +143,7 @@ export const zhCNObjects: NonNullable = { }, _validations: { reserved_identity_position: { - message: "'position' cannot spell a framework-reserved built-in identity name (platform_admin, org_owner, org_admin, org_member). These names are ADR-0068 built-in identities: the platform projects them into current_user.positions from their own sources of truth, and a row spelling one is not an assignment of that identity. Choose a different name." + message: "'position' cannot spell a framework-reserved built-in identity name (platform_admin, org_owner, org_admin, org_member). These names are built-in identities: the platform projects them into current_user.positions from their own sources of truth, and a row spelling one is not an assignment of that identity. Choose a different name." } } } diff --git a/packages/plugins/plugin-security/src/translations/zh-CN.source-hashes.generated.ts b/packages/plugins/plugin-security/src/translations/zh-CN.source-hashes.generated.ts index 6444f5e4e3a..8261e8e59af 100644 --- a/packages/plugins/plugin-security/src/translations/zh-CN.source-hashes.generated.ts +++ b/packages/plugins/plugin-security/src/translations/zh-CN.source-hashes.generated.ts @@ -21,7 +21,7 @@ export const zhCNGeneratedSourceHashes: Readonly> = { "objects.sys_user_permission_set._validations.validity_window_order.message": "b2216aacc3b6f97b", "objects.sys_user_permission_set.fields.display_title.help": "ee7ef063488cd6c3", "objects.sys_user_permission_set.fields.display_title.label": "70f7aadecce647a5", - "objects.sys_user_position._validations.reserved_identity_position.message": "f35df6c1ef1493b0", + "objects.sys_user_position._validations.reserved_identity_position.message": "5cb33eea311d7306", "objects.sys_user_position.fields.display_title.help": "39b5571b48e7d800", "objects.sys_user_position.fields.display_title.label": "70f7aadecce647a5", }; diff --git a/packages/plugins/plugin-sharing/src/objects/admin-copy-vocabulary.test.ts b/packages/plugins/plugin-sharing/src/objects/admin-copy-vocabulary.test.ts new file mode 100644 index 00000000000..fdae10e667f --- /dev/null +++ b/packages/plugins/plugin-sharing/src/objects/admin-copy-vocabulary.test.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Admin-facing copy of the sharing objects carries no design-record + * vocabulary. + * + * Setup opens two of these objects from Access Control (Sharing Rules and + * Record Shares); 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 two Setup opens: a nav entry added later inherits the rule with nothing + * to update. (`sys_business_unit` and its member object, which this plugin + * also registers, are `@objectstack/platform-objects`' and are judged there.) + * + * The same rule over the other Setup producers lives in each of them + * (`@objectstack/platform-objects`' `src/apps/setup-copy-vocabulary.test.ts`, + * `@objectstack/service-messaging`'s `src/objects/admin-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 SharingObjects 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('sharing objects: admin-facing copy carries no design-record vocabulary', () => { + it('the matcher recognises each kind of vocabulary it exists to keep out', () => { + expect(vocabularyIn('Sharing grant (ADR-0052 §5).')).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('Records shared beyond their owner, one row per grant.')).toEqual([]); + }); + + it('object, field and view copy of every object this package exports', () => { + const objects: ObjectLike[] = (Object.values(SharingObjects) as unknown[]).filter(isObjectSchema); + const names = objects.map((o) => o.name); + for (const name of ['sys_record_share', 'sys_sharing_rule', 'sys_share_link']) { + 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/plugins/plugin-sharing/src/objects/sys-record-share.object.ts b/packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts index b2faffca45e..6b889905798 100644 --- a/packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts +++ b/packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts @@ -39,7 +39,9 @@ export const SysRecordShare = ObjectSchema.create({ icon: 'share', isSystem: true, managedBy: 'engine-owned', - description: 'Per-record sharing grant — extends OWD with explicit access', + // Each row widens one record beyond the object's sharing model (its OWD + // baseline) for one recipient; the sentence below is what Setup shows. + description: 'Records shared beyond their owner, one row per grant.', titleFormat: '{object_name}/{record_id} → {recipient_id} ({access_level})', highlightFields: ['object_name', 'record_id', 'recipient_id', 'access_level', 'source'], diff --git a/packages/plugins/plugin-sharing/src/translations/en.objects.generated.ts b/packages/plugins/plugin-sharing/src/translations/en.objects.generated.ts index 91bc66e89c9..dfdb2e3710e 100644 --- a/packages/plugins/plugin-sharing/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-sharing/src/translations/en.objects.generated.ts @@ -18,7 +18,7 @@ export const enObjects: NonNullable = { sys_record_share: { label: "Record Share", pluralLabel: "Record Shares", - description: "Per-record sharing grant — extends OWD with explicit access", + description: "Records shared beyond their owner, one row per grant.", fields: { id: { label: "Share ID" diff --git a/packages/plugins/plugin-sharing/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-sharing/src/translations/es-ES.objects.generated.ts index f6e04ca6e95..fd64df1b7fd 100644 --- a/packages/plugins/plugin-sharing/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-sharing/src/translations/es-ES.objects.generated.ts @@ -18,7 +18,7 @@ export const esESObjects: NonNullable = { sys_record_share: { label: "Compartición de registro", pluralLabel: "Comparticiones de registro", - description: "Concesión de compartición por registro; amplía OWD con acceso explícito.", + description: "Registros compartidos más allá de su propietario, una fila por concesión.", fields: { id: { label: "ID de compartición" diff --git a/packages/plugins/plugin-sharing/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-sharing/src/translations/ja-JP.objects.generated.ts index 0b5e05abd97..a310cc8918f 100644 --- a/packages/plugins/plugin-sharing/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-sharing/src/translations/ja-JP.objects.generated.ts @@ -18,7 +18,7 @@ export const jaJPObjects: NonNullable = { sys_record_share: { label: "レコード共有", pluralLabel: "レコード共有", - description: "レコードごとの共有付与 — OWD に明示的なアクセスを追加", + description: "所有者以外にも共有されたレコード。付与ごとに 1 行です。", fields: { id: { label: "共有 ID" diff --git a/packages/plugins/plugin-sharing/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-sharing/src/translations/zh-CN.objects.generated.ts index 1cb1048573c..ede8641e577 100644 --- a/packages/plugins/plugin-sharing/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-sharing/src/translations/zh-CN.objects.generated.ts @@ -18,7 +18,7 @@ export const zhCNObjects: NonNullable = { sys_record_share: { label: "记录共享", pluralLabel: "记录共享", - description: "按记录粒度的共享授权——在 OWD 基础上提供显式访问", + description: "共享给所有者以外人员的记录,每条授权一行。", fields: { id: { label: "共享 ID" diff --git a/packages/services/service-automation/src/admin-copy-vocabulary.test.ts b/packages/services/service-automation/src/admin-copy-vocabulary.test.ts new file mode 100644 index 00000000000..c29c46b6b9f --- /dev/null +++ b/packages/services/service-automation/src/admin-copy-vocabulary.test.ts @@ -0,0 +1,91 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Admin-facing copy of the automation objects carries no design-record + * vocabulary. + * + * The sentence under an object page's title is the object's `description`, + * and a field's help tooltip is the field's `description`. 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. Setup has no nav entry for these objects + * today; every object this package ships is judged anyway, so a page or nav + * entry added later inherits the rule with nothing to update. This package + * generates no translation bundle, so there is no bundle leg here. + * + * The same rule over the other Setup producers lives in each of them + * (`@objectstack/platform-objects`' `src/apps/setup-copy-vocabulary.test.ts`, + * `@objectstack/service-messaging`'s `src/objects/admin-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 Automation from './index.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 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'; +} + +describe('automation objects: admin-facing copy carries no design-record vocabulary', () => { + it('the matcher recognises each kind of vocabulary it exists to keep out', () => { + expect(vocabularyIn('Live suspended runs (resumable, ADR-0019).')).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('Never re-launched by a rebuilt kernel.')).toEqual(['a runtime internal']); + expect(vocabularyIn('Paused automation runs waiting to resume.')).toEqual([]); + }); + + it('object, field and view copy of every object this package exports', () => { + const objects: ObjectLike[] = (Object.values(Automation) as unknown[]).filter(isObjectSchema); + const names = objects.map((o) => o.name); + for (const name of ['sys_automation_run', 'sys_flow_dispatch', 'sys_flow_credential']) { + 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([]); + }); +}); diff --git a/packages/services/service-automation/src/sys-automation-run.object.ts b/packages/services/service-automation/src/sys-automation-run.object.ts index 5a0f59cde96..bb439d7bb75 100644 --- a/packages/services/service-automation/src/sys-automation-run.object.ts +++ b/packages/services/service-automation/src/sys-automation-run.object.ts @@ -67,7 +67,10 @@ export const SysAutomationRun = ObjectSchema.create({ onlyWhen: { status: { $in: ['completed', 'failed', 'cancelled', 'timed_out', 'refused'] } }, }, }, - description: 'Durable automation run state: live suspended runs (resumable, ADR-0019) and terminal run history (completed / failed / cancelled / timed_out / refused, for observability).', + // [ADR-0019] Paused runs are resumable state; the terminal rows are + // observability history (completed / failed / cancelled / timed_out / + // refused), aged out by the retention above. + description: 'Paused automation runs waiting to resume, and the recent history of finished runs.', // [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-automation/src/sys-flow-dispatch.object.ts b/packages/services/service-automation/src/sys-flow-dispatch.object.ts index f27ed0d22c0..af99f685a79 100644 --- a/packages/services/service-automation/src/sys-flow-dispatch.object.ts +++ b/packages/services/service-automation/src/sys-flow-dispatch.object.ts @@ -92,7 +92,7 @@ export const SysFlowDispatch = ObjectSchema.create({ retention: { maxAge: '30d' }, }, description: - 'Idempotency ledger for trigger dispatches: one row per claimed dispatch key — (flow, record, matched-window) for a time-relative sweep, (flow, tick-window) for a scheduled flow — so a re-scan, a rebuilt kernel or an operator replay never re-launches a flow for a window it already delivered.', + 'Idempotency ledger for trigger dispatches: one row per claimed dispatch key — (flow, record, matched-window) for a time-relative sweep, (flow, tick-window) for a scheduled flow — so a re-scan, a reload or restart, or an operator replay never re-launches a flow for a window it already delivered.', displayNameField: 'id', nameField: 'id', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) highlightFields: ['id', 'dispatched_at', 'outcome'],