Skip to content

Commit 8a91c55

Browse files
committed
fix(platform-objects, plugin-security, plugin-approvals, service-messaging, objectql): Setup field help written for administrators; the inert access-review pair hidden
Claude-Session: https://claude.ai/code/session_01RsXNmkeex9WY5XsjNwtRYv Co-authored-by: Claude <noreply@anthropic.com>
1 parent 303aef0 commit 8a91c55

16 files changed

Lines changed: 172 additions & 46 deletions

‎packages/metadata-core/src/objects/sys-metadata.object.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const SysMetadataObject = ObjectSchema.create({
7777
label: 'Package Version',
7878
required: false,
7979
description:
80-
'Foreign key to sys_package_version (UUID). Null = platform-built-in or env-level override.',
80+
'The package release this metadata was installed from. Empty for built-in platform metadata and for changes made in this environment.',
8181
}),
8282

8383
/** Who manages this record: package, platform, or user */
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The Data Lifecycle settings page is administrator copy.
5+
*
6+
* Setup renders the manifest's `description` under the page title and each
7+
* specifier's `label` / `description` beside its control. An ADR number, a
8+
* section reference or the name of an internal component tells that reader
9+
* nothing they can act on: the reference belongs in the docblock next to the
10+
* manifest, never in the string.
11+
*/
12+
13+
import { describe, expect, it } from 'vitest';
14+
import { lifecycleSettingsManifest } from './lifecycle-settings.js';
15+
16+
const DESIGN_RECORD_VOCABULARY: ReadonlyArray<readonly [string, RegExp]> = [
17+
['an ADR number', /ADR-\d/],
18+
['a section reference', /§\s?\d/],
19+
['an internal component', /\bLifecycleService\b|\bReaper\b|\bRotator\b|\bArchiver\b/],
20+
];
21+
22+
function vocabularyIn(text: string): string[] {
23+
return DESIGN_RECORD_VOCABULARY.filter(([, re]) => re.test(text)).map(([name]) => name);
24+
}
25+
26+
type Copy = { label?: string; description?: string; key?: string };
27+
28+
describe('lifecycle settings: admin-facing copy carries no design-record vocabulary', () => {
29+
it('the matcher recognises each kind of vocabulary it exists to keep out', () => {
30+
expect(vocabularyIn('Governance for ADR-0057 data lifecycle enforcement.')).toEqual(['an ADR number']);
31+
expect(vocabularyIn('A regulated tenant sets years (§3.2).')).toEqual(['a section reference']);
32+
expect(vocabularyIn('Growth alerts for the LifecycleService (Reaper / Rotator / Archiver).')).toEqual(['an internal component']);
33+
expect(vocabularyIn('How long system data is kept.')).toEqual([]);
34+
});
35+
36+
it('the page description and every specifier label and description', () => {
37+
const manifest = lifecycleSettingsManifest as unknown as Copy & { specifiers: Copy[] };
38+
const strings: Array<[string, string]> = [];
39+
for (const [at, item] of [['lifecycle', manifest] as const, ...manifest.specifiers.map((s) => [`lifecycle.${s.key}`, s] as const)]) {
40+
if (typeof item.label === 'string') strings.push([`${at}.label`, item.label]);
41+
if (typeof item.description === 'string') strings.push([`${at}.description`, item.description]);
42+
}
43+
// Positive control: the walk reaches the page sentence and the retention override help.
44+
expect(strings.map(([at]) => at)).toEqual(expect.arrayContaining(['lifecycle.description', 'lifecycle.retention_overrides.description']));
45+
const offenders = strings.filter(([, text]) => vocabularyIn(text).length > 0).map(([at, text]) => `${at}: ${text}`);
46+
expect(offenders).toEqual([]);
47+
});
48+
});

‎packages/objectql/src/lifecycle/lifecycle-settings.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export const lifecycleSettingsManifest = {
1717
label: 'Data Lifecycle',
1818
icon: 'Timer',
1919
description:
20-
'Governance for ADR-0057 data lifecycle enforcement: retention window ' +
21-
'overrides, per-table row quotas, and growth alerts for the platform ' +
22-
'LifecycleService (Reaper / Rotator / Archiver).',
20+
'How long system data is kept: override retention windows, cap row counts ' +
21+
'per table, and get alerted when a table grows unexpectedly. The platform ' +
22+
'applies these on its periodic cleanup sweep.',
2323
scope: 'global',
2424
readPermission: 'manage_platform_settings',
2525
writePermission: 'manage_platform_settings',
@@ -44,9 +44,9 @@ export const lifecycleSettingsManifest = {
4444
default: {},
4545
description:
4646
'Per-object window overrides: { "<object>": { "maxAge": "1y", "expireAfter": "30d" } }. ' +
47-
'Duration literals: h/d/w/y. Tenant-scoped — a regulated tenant sets years while dev keeps days (ADR-0057 §3.2). ' +
48-
'An override BELOW a retention floor a consumer registered (e.g. the job queue\'s dedup window) is rejected at ' +
49-
'sweep time and logged at error — the declared window keeps running.',
47+
'Durations use h/d/w/y. Set per organization, so a regulated organization can keep years while a development one keeps days. ' +
48+
'An override shorter than the minimum a platform feature needs (for example the job queue\'s duplicate-detection window) ' +
49+
'is ignored and logged as an error, and the default window keeps applying.',
5050
},
5151
{
5252
type: 'json',

‎packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ const DESIGN_RECORD_VOCABULARY: ReadonlyArray<readonly [string, RegExp]> = [
6161
['a review-finding id', /finding F/],
6262
['an API call', /\bemit\(\)/],
6363
['a runtime internal', /\bkernel\b/i],
64+
// The developer notes a Setup pass met in field help: a column reference,
65+
// a function name, an internal component, a storage-history note and an
66+
// inert-column notice. What the field means and what to do with it is the
67+
// help; where it is stored and which code reads it is the comment.
68+
['a foreign-key note', /\bforeign key to\b/i],
69+
['a function name', /\bisGrantActive\b/],
70+
['an internal component', /\bmiddleware\b|\bbetter-auth is oblivious\b|\bReaper\b/i],
71+
['a column-history note', /\bNULL only on\b/],
72+
['an inert-column notice', /\bInert today\b/i],
6473
];
6574

6675
function vocabularyIn(text: string): string[] {
@@ -133,6 +142,11 @@ describe('Setup copy carries no design-record vocabulary', () => {
133142
expect(vocabularyIn('Bypass rule (finding F2).')).toEqual(['a review-finding id']);
134143
expect(vocabularyIn('One row per emit().')).toEqual(['an API call']);
135144
expect(vocabularyIn('The Kernel API is additive.')).toEqual(['a runtime internal']);
145+
expect(vocabularyIn('Foreign key to sys_user.id')).toEqual(['a foreign-key note']);
146+
expect(vocabularyIn('Enforced at resolution time via isGrantActive.')).toEqual(['a function name']);
147+
expect(vocabularyIn('Batch cadence (P3 digest middleware).')).toEqual(['an internal component']);
148+
expect(vocabularyIn('NULL only on a grant written before the column existed.')).toEqual(['a column-history note']);
149+
expect(vocabularyIn('Inert today: nothing reads it.')).toEqual(['an inert-column notice']);
136150
expect(vocabularyIn('Your company structure, used to scope data and reports.')).toEqual([]);
137151
});
138152

‎packages/platform-objects/src/identity/sys-oauth-access-token.object.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,25 +83,25 @@ export const SysOauthAccessToken = ObjectSchema.create({
8383
// unique string — varchar(255) on MySQL). A referencing column takes the
8484
// referenced column's bound.
8585
maxLength: 255,
86-
description: 'Foreign key to sys_oauth_application.client_id',
86+
description: 'The client ID of the OAuth application this token was issued to.',
8787
}),
8888

8989
session_id: Field.lookup('sys_session', {
9090
label: 'Session',
9191
required: false,
92-
description: 'Foreign key to sys_session.id',
92+
description: 'The sign-in session this token was issued in.',
9393
}),
9494

9595
user_id: Field.lookup('sys_user', {
9696
label: 'User',
9797
required: false,
98-
description: 'Foreign key to sys_user.id',
98+
description: 'The user this token acts for.',
9999
}),
100100

101101
refresh_id: Field.lookup('sys_oauth_refresh_token', {
102102
label: 'Refresh Token',
103103
required: false,
104-
description: 'Foreign key to sys_oauth_refresh_token.id',
104+
description: 'The refresh token this access token was issued from.',
105105
}),
106106

107107
reference_id: Field.text({

‎packages/platform-objects/src/identity/sys-oauth-client-resource.object.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const SysOauthClientResource = ObjectSchema.create({
4646
// unique string — varchar(255) on MySQL). A referencing column takes the
4747
// referenced column's bound.
4848
maxLength: 255,
49-
description: 'Foreign key to sys_oauth_application.client_id',
49+
description: 'The client ID of the OAuth application this grant is for.',
5050
}),
5151

5252
resource_id: Field.text({
@@ -91,7 +91,7 @@ export const SysOauthClientResource = ObjectSchema.create({
9191
// symmetric: referent and referrer now accept exactly the same domain,
9292
// and the silent register-then-never-authorize dead-end is closed.
9393
maxLength: 255,
94-
description: 'Foreign key to sys_oauth_resource.identifier',
94+
description: 'The identifier of the protected resource the application may request tokens for.',
9595
}),
9696

9797
metadata: Field.textarea({

‎packages/platform-objects/src/identity/sys-oauth-consent.object.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,13 @@ export const SysOauthConsent = ObjectSchema.create({
6464
// unique string — varchar(255) on MySQL). A referencing column takes the
6565
// referenced column's bound.
6666
maxLength: 255,
67-
description: 'Foreign key to sys_oauth_application.client_id',
67+
description: 'The client ID of the OAuth application the user consented to.',
6868
}),
6969

7070
user_id: Field.lookup('sys_user', {
7171
label: 'User',
7272
required: false,
73-
description: 'Foreign key to sys_user.id',
73+
description: 'The user who gave this consent.',
7474
}),
7575

7676
reference_id: Field.text({

‎packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,19 @@ export const SysOauthRefreshToken = ObjectSchema.create({
8282
// unique string — varchar(255) on MySQL). A referencing column takes the
8383
// referenced column's bound.
8484
maxLength: 255,
85-
description: 'Foreign key to sys_oauth_application.client_id',
85+
description: 'The client ID of the OAuth application this token was issued to.',
8686
}),
8787

8888
session_id: Field.lookup('sys_session', {
8989
label: 'Session',
9090
required: false,
91-
description: 'Foreign key to sys_session.id',
91+
description: 'The sign-in session this token was issued in.',
9292
}),
9393

9494
user_id: Field.lookup('sys_user', {
9595
label: 'User',
9696
required: true,
97-
description: 'Foreign key to sys_user.id',
97+
description: 'The user this token acts for.',
9898
}),
9999

100100
reference_id: Field.text({

‎packages/platform-objects/src/identity/sys-user.object.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -967,13 +967,14 @@ export const SysUser = ObjectSchema.create({
967967
defaultValue: false,
968968
readonly: true, // ADR-0092 — a licensed-seat grant; flows through the AiSeatPlugin enforcement path
969969
group: 'Admin',
970+
// The framework synthesizes the `ai_seat` capability from this flag
971+
// (plugin-hono-server resolveCtx); the seat cap is enforced by
972+
// @objectstack/security-enterprise AiSeatPlugin. Owned by objectql
973+
// (better-auth is oblivious to this column).
970974
description:
971-
'Whether this user holds an AI seat — grants access to the in-UI AI ' +
972-
'agents (build / ask). The framework synthesizes the `ai_seat` ' +
973-
'capability from this flag (plugin-hono-server resolveCtx). Assignment ' +
974-
'is capped by the licensed / purchased seat count (enforced by ' +
975-
'@objectstack/security-enterprise AiSeatPlugin). Owned by objectql ' +
976-
'(better-auth is oblivious to this column).',
975+
'Whether this user holds an AI seat, which gives access to the AI ' +
976+
'assistants in the app (build and ask). How many users can hold one ' +
977+
'is limited by the seats your license includes.',
977978
}),
978979

979980
// ── Profile ──────────────────────────────────────────────────
@@ -1033,9 +1034,8 @@ export const SysUser = ObjectSchema.create({
10331034
maxLength: 35,
10341035
group: 'Profile',
10351036
description:
1036-
'Preferred language for notifications, as a BCP-47 tag (e.g. zh-CN, ja-JP). ' +
1037-
'Read per recipient at delivery time; when unset the deployment default applies. ' +
1038-
'Owned by objectql (better-auth is oblivious to this column).',
1037+
'Preferred language for this user\'s notifications, as a language tag such as zh-CN or ja-JP. ' +
1038+
'When unset, the deployment\'s default language applies.',
10391039
}),
10401040

10411041
// ── Organization ─────────────────────────────────────────────

‎packages/plugins/plugin-approvals/src/admin-copy-vocabulary.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ const DESIGN_RECORD_VOCABULARY: ReadonlyArray<readonly [string, RegExp]> = [
3737
['a review-finding id', /finding F/],
3838
['an API call', /\bemit\(\)/],
3939
['a runtime internal', /\bkernel\b/i],
40+
// The developer notes a Setup pass met in field help: a column reference,
41+
// a function name, an internal component, a storage-history note and an
42+
// inert-column notice. What the field means and what to do with it is the
43+
// help; where it is stored and which code reads it is the comment.
44+
['a foreign-key note', /\bforeign key to\b/i],
45+
['a function name', /\bisGrantActive\b/],
46+
['an internal component', /\bmiddleware\b|\bbetter-auth is oblivious\b|\bReaper\b/i],
47+
['a column-history note', /\bNULL only on\b/],
48+
['an inert-column notice', /\bInert today\b/i],
4049
];
4150

4251
function vocabularyIn(text: string): string[] {
@@ -93,6 +102,11 @@ describe('approval objects: admin-facing copy carries no design-record vocabular
93102
expect(vocabularyIn('Bypass rule (finding F2).')).toEqual(['a review-finding id']);
94103
expect(vocabularyIn('One row per emit().')).toEqual(['an API call']);
95104
expect(vocabularyIn('Resumed by a rebuilt kernel.')).toEqual(['a runtime internal']);
105+
expect(vocabularyIn('Foreign key to sys_user.id')).toEqual(['a foreign-key note']);
106+
expect(vocabularyIn('Enforced at resolution time via isGrantActive.')).toEqual(['a function name']);
107+
expect(vocabularyIn('Batch cadence (P3 digest middleware).')).toEqual(['an internal component']);
108+
expect(vocabularyIn('NULL only on a grant written before the column existed.')).toEqual(['a column-history note']);
109+
expect(vocabularyIn('Inert today: nothing reads it.')).toEqual(['an inert-column notice']);
96110
expect(vocabularyIn('While someone is away, approvals go to their delegate.')).toEqual([]);
97111
});
98112

0 commit comments

Comments
 (0)