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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/25331-admin-field-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@objectstack/platform-objects': patch
'@objectstack/metadata-core': patch
'@objectstack/plugin-security': patch
'@objectstack/plugin-approvals': patch
'@objectstack/service-messaging': patch
'@objectstack/objectql': patch
---

fix: Setup field help is written for administrators, and the two access-review fields nothing uses yet are hidden from the Setup forms

Clause-②: no
Metadata-protocol: none

A Setup pass met field help written for the platform's developers: "Foreign key to sys_user.id", "NULL only on a grant written before the column existed", "via isGrantActive", "(P3 digest middleware)", "Owned by objectql (better-auth is oblivious to this column)", "LifecycleService (Reaper / Rotator / Archiver)" and ADR numbers. Each now says what the field means and what to do with it, in every shipped locale (`en` regenerated from the source; `zh-CN`, `ja-JP` and `es-ES` translated):

- the user, OAuth application, session and refresh-token fields of the permission-set and position assignments and of the OAuth token, consent and client-resource objects, and the package-version field of stored metadata;
- the permission set picked on an assignment;
- the start time of an approval delegation;
- the digest cadence and quiet hours of a notification preference, which now say how each value behaves;
- the AI Access and Locale fields of a user;
- the Data Lifecycle settings page and its retention overrides.

`Last Certified At` and `Certified By` on `sys_user_permission_set` and `sys_user_position` are reserved for a future access-review feature and nothing writes or reads them. They are now `hidden: true`, so the Setup forms and detail pages no longer show them. The columns and any stored values stay.
2 changes: 1 addition & 1 deletion packages/metadata-core/src/objects/sys-metadata.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const SysMetadataObject = ObjectSchema.create({
label: 'Package Version',
required: false,
description:
'Foreign key to sys_package_version (UUID). Null = platform-built-in or env-level override.',
'The package release this metadata was installed from. Empty for built-in platform metadata and for changes made in this environment.',
}),

/** Who manages this record: package, platform, or user */
Expand Down
48 changes: 48 additions & 0 deletions packages/objectql/src/lifecycle/lifecycle-settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The Data Lifecycle settings page is administrator copy.
*
* Setup renders the manifest's `description` under the page title and each
* specifier's `label` / `description` beside its control. An ADR number, a
* section reference or the name of an internal component tells that reader
* nothing they can act on: the reference belongs in the docblock next to the
* manifest, never in the string.
*/

import { describe, expect, it } from 'vitest';
import { lifecycleSettingsManifest } from './lifecycle-settings.js';

const DESIGN_RECORD_VOCABULARY: ReadonlyArray<readonly [string, RegExp]> = [
['an ADR number', /ADR-\d/],
['a section reference', /§\s?\d/],
['an internal component', /\bLifecycleService\b|\bReaper\b|\bRotator\b|\bArchiver\b/],
];

function vocabularyIn(text: string): string[] {
return DESIGN_RECORD_VOCABULARY.filter(([, re]) => re.test(text)).map(([name]) => name);
}

type Copy = { label?: string; description?: string; key?: string };

describe('lifecycle settings: admin-facing copy carries no design-record vocabulary', () => {
it('the matcher recognises each kind of vocabulary it exists to keep out', () => {
expect(vocabularyIn('Governance for ADR-0057 data lifecycle enforcement.')).toEqual(['an ADR number']);
expect(vocabularyIn('A regulated tenant sets years (§3.2).')).toEqual(['a section reference']);
expect(vocabularyIn('Growth alerts for the LifecycleService (Reaper / Rotator / Archiver).')).toEqual(['an internal component']);
expect(vocabularyIn('How long system data is kept.')).toEqual([]);
});

it('the page description and every specifier label and description', () => {
const manifest = lifecycleSettingsManifest as unknown as Copy & { specifiers: Copy[] };
const strings: Array<[string, string]> = [];
for (const [at, item] of [['lifecycle', manifest] as const, ...manifest.specifiers.map((s) => [`lifecycle.${s.key}`, s] as const)]) {
if (typeof item.label === 'string') strings.push([`${at}.label`, item.label]);
if (typeof item.description === 'string') strings.push([`${at}.description`, item.description]);
}
// Positive control: the walk reaches the page sentence and the retention override help.
expect(strings.map(([at]) => at)).toEqual(expect.arrayContaining(['lifecycle.description', 'lifecycle.retention_overrides.description']));
const offenders = strings.filter(([, text]) => vocabularyIn(text).length > 0).map(([at, text]) => `${at}: ${text}`);
expect(offenders).toEqual([]);
});
});
12 changes: 6 additions & 6 deletions packages/objectql/src/lifecycle/lifecycle-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export const lifecycleSettingsManifest = {
label: 'Data Lifecycle',
icon: 'Timer',
description:
'Governance for ADR-0057 data lifecycle enforcement: retention window ' +
'overrides, per-table row quotas, and growth alerts for the platform ' +
'LifecycleService (Reaper / Rotator / Archiver).',
'How long system data is kept: override retention windows, cap row counts ' +
'per table, and get alerted when a table grows unexpectedly. The platform ' +
'applies these on its periodic cleanup sweep.',
scope: 'global',
readPermission: 'manage_platform_settings',
writePermission: 'manage_platform_settings',
Expand All @@ -44,9 +44,9 @@ export const lifecycleSettingsManifest = {
default: {},
description:
'Per-object window overrides: { "<object>": { "maxAge": "1y", "expireAfter": "30d" } }. ' +
'Duration literals: h/d/w/y. Tenant-scoped — a regulated tenant sets years while dev keeps days (ADR-0057 §3.2). ' +
'An override BELOW a retention floor a consumer registered (e.g. the job queue\'s dedup window) is rejected at ' +
'sweep time and logged at error — the declared window keeps running.',
'Durations use h/d/w/y. Set per organization, so a regulated organization can keep years while a development one keeps days. ' +
'An override shorter than the minimum a platform feature needs (for example the job queue\'s duplicate-detection window) ' +
'is ignored and logged as an error, and the default window keeps applying.',
},
{
type: 'json',
Expand Down
14 changes: 14 additions & 0 deletions packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ const DESIGN_RECORD_VOCABULARY: ReadonlyArray<readonly [string, RegExp]> = [
['a review-finding id', /finding F/],
['an API call', /\bemit\(\)/],
['a runtime internal', /\bkernel\b/i],
// The developer notes a Setup pass met in field help: a column reference,
// a function name, an internal component, a storage-history note and an
// inert-column notice. What the field means and what to do with it is the
// help; where it is stored and which code reads it is the comment.
['a foreign-key note', /\bforeign key to\b/i],
['a function name', /\bisGrantActive\b/],
['an internal component', /\bmiddleware\b|\bbetter-auth is oblivious\b|\bReaper\b/i],
['a column-history note', /\bNULL only on\b/],
['an inert-column notice', /\bInert today\b/i],
];

function vocabularyIn(text: string): string[] {
Expand Down Expand Up @@ -133,6 +142,11 @@ describe('Setup copy carries no design-record vocabulary', () => {
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('Foreign key to sys_user.id')).toEqual(['a foreign-key note']);
expect(vocabularyIn('Enforced at resolution time via isGrantActive.')).toEqual(['a function name']);
expect(vocabularyIn('Batch cadence (P3 digest middleware).')).toEqual(['an internal component']);
expect(vocabularyIn('NULL only on a grant written before the column existed.')).toEqual(['a column-history note']);
expect(vocabularyIn('Inert today: nothing reads it.')).toEqual(['an inert-column notice']);
expect(vocabularyIn('Your company structure, used to scope data and reports.')).toEqual([]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
},
ai_access: {
label: "AI Access",
help: "Whether this user holds an AI seat — grants access to the in-UI AI agents (build / ask). The framework synthesizes the `ai_seat` capability from this flag (plugin-hono-server resolveCtx). Assignment is capped by the licensed / purchased seat count (enforced by @objectstack/security-enterprise AiSeatPlugin). Owned by objectql (better-auth is oblivious to this column)."
help: "Whether this user holds an AI seat. A seat grants the `ai_seat` capability, which opens the AI assistants in the app (build and ask). How many users can hold one is limited by the seats your license includes."
},
image: {
label: "Profile Image"
},
locale: {
label: "Locale",
help: "Preferred language for notifications, as a BCP-47 tag (e.g. zh-CN, ja-JP). Read per recipient at delivery time; when unset the deployment default applies. Owned by objectql (better-auth is oblivious to this column)."
help: "Preferred language for this user's notifications, as a language tag such as zh-CN or ja-JP. When unset, the deployment's default language applies."
},
manager_id: {
label: "Manager",
Expand Down Expand Up @@ -1582,19 +1582,19 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
},
client_id: {
label: "Client ID",
help: "Foreign key to sys_oauth_application.client_id"
help: "The client ID of the OAuth application this token was issued to."
},
session_id: {
label: "Session",
help: "Foreign key to sys_session.id"
help: "The sign-in session this token was issued in."
},
user_id: {
label: "User",
help: "Foreign key to sys_user.id"
help: "The user this token acts for."
},
refresh_id: {
label: "Refresh Token",
help: "Foreign key to sys_oauth_refresh_token.id"
help: "The refresh token this access token was issued from."
},
reference_id: {
label: "Reference ID",
Expand Down Expand Up @@ -1646,15 +1646,15 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
},
client_id: {
label: "Client ID",
help: "Foreign key to sys_oauth_application.client_id"
help: "The client ID of the OAuth application this token was issued to."
},
session_id: {
label: "Session",
help: "Foreign key to sys_session.id"
help: "The sign-in session this token was issued in."
},
user_id: {
label: "User",
help: "Foreign key to sys_user.id"
help: "The user this token acts for."
},
reference_id: {
label: "Reference ID",
Expand Down Expand Up @@ -1718,11 +1718,11 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
},
client_id: {
label: "Client ID",
help: "Foreign key to sys_oauth_application.client_id"
help: "The client ID of the OAuth application the user consented to."
},
user_id: {
label: "User",
help: "Foreign key to sys_user.id"
help: "The user who gave this consent."
},
reference_id: {
label: "Reference ID",
Expand Down Expand Up @@ -1820,11 +1820,11 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
},
client_id: {
label: "Client ID",
help: "Foreign key to sys_oauth_application.client_id"
help: "The client ID of the OAuth application this grant is for."
},
resource_id: {
label: "Resource ID",
help: "Foreign key to sys_oauth_resource.identifier"
help: "The identifier of the protected resource the application may request tokens for."
},
metadata: {
label: "Metadata",
Expand Down Expand Up @@ -3042,7 +3042,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
},
package_version_id: {
label: "Package Version",
help: "Foreign key to sys_package_version (UUID). Null = platform-built-in or env-level override."
help: "The package release this metadata was installed from. Empty for built-in platform metadata and for changes made in this environment."
},
managed_by: {
label: "Managed By",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
},
ai_access: {
label: "Acceso a IA",
help: "Indica si este usuario dispone de una plaza de IA: otorga acceso a los agentes de IA integrados en la interfaz (build / ask). El framework sintetiza la capacidad `ai_seat` a partir de este indicador (plugin-hono-server resolveCtx). La asignación está limitada por el número de plazas licenciadas o compradas (aplicado por AiSeatPlugin de @objectstack/security-enterprise). Propiedad de objectql (better-auth ignora esta columna)."
help: "Indica si este usuario dispone de una plaza de IA. La plaza otorga la capacidad `ai_seat`, que da acceso a los asistentes de IA de la aplicación (build y ask). El número de usuarios que pueden tenerla está limitado por las plazas que incluye su licencia."
},
image: {
label: "Imagen de perfil"
},
locale: {
label: "Idioma",
help: "Idioma preferido para las notificaciones, como etiqueta BCP-47 (p. ej. zh-CN, ja-JP). Se lee por destinatario en el momento de la entrega; si no está definido, se aplica el idioma predeterminado del despliegue. Propiedad de objectql (better-auth ignora esta columna)."
help: "Idioma preferido para las notificaciones de este usuario, como una etiqueta de idioma, por ejemplo zh-CN o ja-JP. Si no se define, se aplica el idioma predeterminado del despliegue."
},
manager_id: {
label: "Gerente",
Expand Down Expand Up @@ -1582,19 +1582,19 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
},
client_id: {
label: "ID de cliente",
help: "Clave foránea a sys_oauth_application.client_id."
help: "El ID de cliente de la aplicación OAuth a la que se emitió este token."
},
session_id: {
label: "Sesión",
help: "Clave foránea a sys_session.id."
help: "La sesión de inicio de sesión en la que se emitió este token."
},
user_id: {
label: "Usuario",
help: "Clave foránea a sys_user.id."
help: "El usuario en cuyo nombre actúa este token."
},
refresh_id: {
label: "Token de actualización",
help: "Clave foránea a sys_oauth_refresh_token.id."
help: "El token de actualización a partir del cual se emitió este token de acceso."
},
reference_id: {
label: "ID de referencia",
Expand Down Expand Up @@ -1646,15 +1646,15 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
},
client_id: {
label: "ID de cliente",
help: "Clave foránea a sys_oauth_application.client_id."
help: "El ID de cliente de la aplicación OAuth a la que se emitió este token."
},
session_id: {
label: "Sesión",
help: "Clave foránea a sys_session.id."
help: "La sesión de inicio de sesión en la que se emitió este token."
},
user_id: {
label: "Usuario",
help: "Clave foránea a sys_user.id."
help: "El usuario en cuyo nombre actúa este token."
},
reference_id: {
label: "ID de referencia",
Expand Down Expand Up @@ -1718,11 +1718,11 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
},
client_id: {
label: "ID de cliente",
help: "Clave foránea a sys_oauth_application.client_id."
help: "El ID de cliente de la aplicación OAuth a la que el usuario dio su consentimiento."
},
user_id: {
label: "Usuario",
help: "Clave foránea a sys_user.id."
help: "El usuario que dio este consentimiento."
},
reference_id: {
label: "ID de referencia",
Expand Down Expand Up @@ -1820,11 +1820,11 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
},
client_id: {
label: "ID de cliente",
help: "Clave foránea a sys_oauth_application.client_id."
help: "El ID de cliente de la aplicación OAuth a la que corresponde esta concesión."
},
resource_id: {
label: "ID del recurso",
help: "Clave foránea a sys_oauth_resource.identifier."
help: "El identificador del recurso protegido para el que la aplicación puede solicitar tokens."
},
metadata: {
label: "Metadatos",
Expand Down Expand Up @@ -3042,7 +3042,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
},
package_version_id: {
label: "Versión del paquete",
help: "Clave foránea a sys_package_version (UUID). Null = incorporado en la plataforma o sobrescritura a nivel de entorno."
help: "La versión del paquete desde la que se instaló estos metadatos. Vacío para los metadatos integrados en la plataforma y para los cambios hechos en este entorno."
},
managed_by: {
label: "Gestionado por",
Expand Down
Loading
Loading