Skip to content

Commit a00cf99

Browse files
feat(spec,plugin-approvals): enable.approvalsVisibleToReaders, the per-object opt-in for the read-only record-reader approval tier (#22660)
Fixes #22560 Clause-②: yes (widening) ## What this does An object's own metadata can now turn on the ruled read-only record-reader approval tier (#8652). Before this, the only switch was the `ApprovalsPluginOptions.recordReaderVisibleObjects` constructor option. A config-driven app cannot set it, because its host builds the plugin with no options. - **Spec:** `ObjectCapabilities` (the object's `enable` block) gains `approvalsVisibleToReaders: boolean`, default `false`. Its `.describe()` states what `true` grants and to whom: a caller who can read a record of the object sees that record's approval requests and full action history, read-only. This holds on the approvals API and the generic data API alike, on a read that names the record. No approval action is offered. - **Plugin:** `ApprovalService.addRecordReaderVisibleIds` asks a new private `recordReaderTierOn(object)` per call. It answers true if the host's constructor set holds the object, or if the object's live registered definition (`this.engine.getSchema(object)`) declares `enable.approvalsVisibleToReaders === true`. This is the seat's decision B on the round-1 fork (`6094828840`), AGENTS.md "Startup registry reads" cure 1. - `approvals-plugin.ts`: doc text only. The constructor option stays for hosts that build the plugin themselves. The ruling is unchanged (#8652, `5299823744`, maintainer 「同意」): "A user with read access to the target business record may view that record's approval requests and full action history, **read-only** … Enabled by a per-object or plugin-level switch, **default OFF** … **the downstream project opts in**." ### Why the name `approvalsVisibleToReaders` It sits in the `enable` block beside `trackHistory`, `apiEnabled`, `files`, `feeds`, `activities` and `clone`. Like `apiEnabled`, it is a flag phrase that says what turning it on does. It names the subject (approvals), the grant (visible, so read-only and not actionable) and the grantee (readers of the record). A bare `approvals` was rejected. Under `enable`, it reads as "turn approvals on for this object", and an author or an AI would set it to get approval processes. Approval processes need no such flag, so that reading would silently widen visibility instead. ### Where the declaration is read, and why there The service read the option once, in its constructor, which the plugin calls in `start()`. A round-1 kernel probe showed that at that moment the registry holds only objects registered in `init()`. Objects from installed packages (`kernel:ready`), from a later `start()`, from Studio edits and from dev reloads arrive afterwards. A set collected at start would leave those declarations inert. It would also keep a removed declaration in force until restart, an exposure the author believes is closed. So the flag is read where it is used, from the registry as it is at the read. - **Default OFF cost:** at most one in-memory registry lookup per read that names a record, and none for an untargeted read (the inbox). The business record is never probed for an object that declares nothing: the size-0 early return could no longer stand alone, so the check moved behind the object/record test. - **Fails closed:** an engine without `getSchema` (the optional member of `ApprovalEngine`), an unregistered name, a throwing lookup, or any value but literal `true` reads as not declared. - **One visibility definition, both doors:** unchanged. `requestVisibilitySourceOf` hands the same `visibleRequestIds` to `bindRequestReadGate` and `bindRequestChildReadGates`. The flag widens `sys_approval_request`, `sys_approval_action` and `sys_approval_approver` on both doors together. The new pins read all three on both doors. ### Declaration debts - Liveness row `packages/spec/liveness/object.json` → `enable.children.approvalsVisibleToReaders`: `live`, evidence anchored on `approval-service.ts#recordReaderTierOn`. - Studio: the object form's Capabilities section gets the toggle with a help text (`object.form.ts`). The metadata-forms bundles are regenerated by `node scripts/check-i18n-bundles.mjs --write`. The zh-CN, ja-JP and es-ES leaves are hand-written, and `object-collapsed-sections-echo-decisions.test.ts` carries a decided row for each of the two new leaves, with its counts moved (capabilities 9 → 11 leaves). - Generated: `authorable-surface/data.json` and `authorable-defaults/data.json` (from the spec build), `content/docs/references/data/object.mdx` (`gen:docs`) and `liveness/state-counts/object.md` (`gen:liveness-counts`), as `check:generated` named them. `authorable-surface.base.json` is untouched. - Changesets: `@objectstack/spec` minor and `@objectstack/plugin-approvals` minor, each `Clause-②: yes (widening)`; and `@objectstack/platform-objects` patch, `Clause-②: no`, for the form row's translated leaves (contract review round 1 `6096311080`). ## Tests All runs below went through `scripts/pm/os-verify-lock.sh`, and each quotes the run's own summary line. - **`@objectstack/plugin-approvals`**, at `fe81af4642`: `vitest run` → `Test Files 70 passed (70)`, `Tests 1011 passed (1011)`. `typecheck` (tsc + scripts + `check:test-typecheck`) → `VERDICT command-exit 0`. `tsc -p tsconfig.test.json --listFiles` includes `record-reader-opt-in.integration.test.ts` (1 hit). - **`@objectstack/spec`**, at `fe81af4642`: `vitest run --project local` → `Test Files 642 passed (642)`, `Tests 19150 passed | 1 todo`. `typecheck` → exit 0. - **`@objectstack/platform-objects`**, at `e0c562f1a9`: `vitest run` → `Test Files 69 passed (69)`, `Tests 1082 passed (1082)`. `typecheck` → exit 0. At `fe81af4642`, one pin went red: the catalog-wide translated-label control in `object-lifecycle-panel-echo-decisions.test.ts` read 668 against 667, because this PR adds one authored label. `e0c562f1a9` moves that count. `git diff fe81af4 e0c562f` touches only that file, so the spec and plugin readings above stand for the final head. - **New pins**, `record-reader-opt-in.integration.test.ts` (8 cases). It uses a real ObjectQL engine over better-sqlite3, the real `ApprovalsServicePlugin.start()` and the real data-door normalizer. Each pin compares one reading: the approvals door (list, by id, history) plus the data door's list and by-id reads of all three request tables. - An object declaring the flag, with the host option empty: a reader of the record sees everything, read-only. `viewer` is `{can_act: false, …}`; decide, reassign, comment and recall refuse with `FORBIDDEN:`; the data-door row carries no `viewer`. - A reader on another record, and a caller with no read: nothing, and 404 `RECORD_NOT_FOUND` on the data door. - A read that names no record: the inbox is not widened. - Controls: an undeclared object is unchanged, participants are unchanged, and the host option works alone. - Late registration: an object registered after `start()` with the flag widens; re-registered without it, it stops, with no restart. The registry's own answer flipping is asserted as that pin's control. - **Spec pins** (`object.test.ts`): the default is `false`; the key is accepted on `ObjectSchema` and read back `true`; a string value is refused with `invalid_type`. - **Ablations**, through `scripts/ablation-replace.mjs` (wrap mode; anchor must hit; restore proven by blob equality with HEAD and an empty `git diff HEAD`). The subject resolves from `src` by relative import, so no `dist/` leg applies. The mutated file's HEAD blob is `8d7983b7e922`, the same blob at the final head. - (1) The declared-key read replaced by `return false`: `Tests 3 failed | 5 passed (8)`. Red: the declared reader pin, the read-only pin and the late-registration pin. Green, as expected: the cannot-read, inbox and control pins. - (2) The size-0 early return put back before the declared read: `Tests 3 failed | 5 passed (8)`, the same three red. The first attempt at (2) was refused by the tool before any test ran: the replacement contained its own anchor, so the anchor count could not drop. It was re-anchored on the comment line above. Both restores read `blob == HEAD (8d7983b) and git diff HEAD is empty`. - **Gates**: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) at `e0c562f1a9` derived 115 commands. All were run, plus the 48 artifact-roster commands that need no PR context. Result: 161 exit 0, 1 exit 1, 1 NOT MEASURED. `--ran` reconciles: `115 derived, 114 run, 0 NOT-MEASURED, 1 UNRUN` (the unrun one is `check:dual-build-cjs-loads`). - Exit 1: `pnpm check:platform-checklist`, red on `origin/main` `86da194919` too. Its 7 problems are symbol anchors in `docs/qa/platform-checklist/areas/access-security.json` and `attachments-storage.json` naming symbols absent from `metadata-protocol/src/protocol.ts` and `service-storage/src/attachment-access-hooks.ts`. This diff touches none of those files. - `check:dual-build-cjs-loads`: NOT MEASURED, reason: it needs a whole-workspace build, which this dispatch rules out. - Verdict lines: `check:generated` → `✓ All 15 generated artifacts are up to date`. `check:liveness` → `object 54 classified (live 53, planned 1)`, every `path#symbol` anchor resolves. `check:i18n` → `OK (9 package(s) — all bundles in sync…)`. `check:i18n-stale-fill` → `0 stale-fill`. `check:api-surface` → `unchanged ✓`. `check:nul-bytes` → `OK`. `check:type-check-debt` → `none above its recorded number`. ## Acceptance notes - **Kernel probe (throwaway, not committed).** ObjectKernel + ObjectQLPlugin, with producers registering through the manifest service in `init()`, in a `start()` composed after approvals, and on `kernel:ready`. Each object declares the flag. The reading was taken inside approvals' `start()`, after boot, and after the `kernel:ready` producer re-registered its object without the flag: `seenAtStart {init: true, start: false, ready: false}`, `afterBoot {init: true, start: true, ready: true, plain: false, never_registered: false}`, `afterRemoval false`. An engine with no `getSchema` answers `false`. What `getSchema` returns through that door is the authored literal: the declared object's `enable` reads `{"approvalsVisibleToReaders":true}` with no defaults filled in, and an object with no block reads `undefined`. So an absent block or flag reads as the spec default, `false`. - The option's doc linked `ApprovalService.recordReaderVisibleIds`, a member that does not exist. It now links `addRecordReaderVisibleIds`, in the hunk this PR edits anyway. - The en label is the extractor's humanize of the key, "Approvals Visible To Readers" (the form declares no label), like its siblings in that block. --- _Generated by [Claude Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 317cddd commit a00cf99

20 files changed

Lines changed: 604 additions & 30 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
Studio's object form now offers the `approvalsVisibleToReaders` row in its capabilities panel, with its label and help text in the metadata-form translation catalogs for `en`, `zh-CN`, `ja-JP` and `es-ES`.
6+
7+
Clause-②: no
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/plugin-approvals': minor
3+
---
4+
5+
feat(plugin-approvals): the record-reader approval tier honours an object's own `enable.approvalsVisibleToReaders` declaration
6+
7+
Clause-②: yes (widening)
8+
9+
- **Before this,** the read-only record-reader tier could be switched on only through the `recordReaderVisibleObjects` constructor option. A config-driven app, whose host builds the plugin with no options, had no way to reach it.
10+
- **Now** the tier is on for an object when either source turns it on: the host's `recordReaderVisibleObjects`, or the object's own `enable.approvalsVisibleToReaders: true`. With neither, visibility is exactly as before.
11+
- **Read where it is used.** The service reads the declaration from the object's live registered definition on each read that names a record, not once at start. An object registered after boot (an installed package, a Studio edit, a dev reload) takes effect at once. Removing the flag turns the tier off at once, with no restart.
12+
- **Fails closed.** An engine that cannot answer an object definition, an unknown object, or any value other than `true` leaves the tier off. Default OFF costs one in-memory registry lookup on a read that names a record. The business record is never probed for an object that declares nothing.
13+
- **Unchanged.** The rule itself: who counts as a reader, the read-only boundary, the request tables it covers on both doors, and the constructor option.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): `enable.approvalsVisibleToReaders`, the per-object opt-in for read-only approval visibility to a record's readers
6+
7+
Clause-②: yes (widening)
8+
9+
- **The key.** `ObjectCapabilities` (an object's `enable` block) gains `approvalsVisibleToReaders`, a boolean that defaults to `false`. An object that declares nothing is unchanged.
10+
- **What `true` grants.** A caller who can read a record of the object sees that record's approval requests and their full action history, read-only. "Can read" is the object's own CRUD, sharing and RLS, asked as the caller. No new permission is granted. This holds on the approvals API and on the generic data API alike (`sys_approval_request`, `sys_approval_action` and `sys_approval_approver`), on a read that names the record. A list that names no record, such as the inbox, is not widened. No approval action is offered: approve, reject, reassign, recall and comment keep authorizing as before.
11+
- **What becomes visible.** The request row, including its snapshot of the record at submission, and each action's actor, decision, time, comment text and attachments. Turn it on only for objects whose approval commentary the record's readers are meant to see.
12+
- **Studio.** The object form's Capabilities section lists the new toggle.
13+
- **Nothing to migrate.** To opt an object in, write `enable: { approvalsVisibleToReaders: true }` on it.

‎content/docs/references/data/object.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ const result = ApiMethod.parse(data);
430430
| **feeds** | `boolean` | optional (default: `true`) | Record comments/collaboration feed. Default on; explicit false hides the feed UI and rejects any write that makes a comment target this object (403 FEEDS_DISABLED) — a new comment and an update that re-threads an existing one alike |
431431
| **activities** | `boolean` | optional (default: `true`) | Record activity timeline (sys_activity mirror of CRUD). Default on; explicit false stops mirroring and hides the timeline |
432432
| **clone** | `boolean` | optional (default: `true`) | Allow record deep cloning |
433+
| **approvalsVisibleToReaders** | `boolean` | optional (default: `false`) | Opt-in: true lets a caller who can read a record of this object see that record's approval requests and full action history (comments and attachments included), read-only, on the approvals API and the generic data API alike, on a read that names the record. No approval action is offered. Default false: only the request's participants (submitter, approvers, past actors) and administrators see it |
433434

434435
### Nested Shape: `Object.publicSharing`
435436

@@ -533,6 +534,7 @@ const result = ApiMethod.parse(data);
533534
| **feeds** | `boolean` | optional (default: `true`) | Record comments/collaboration feed. Default on; explicit false hides the feed UI and rejects any write that makes a comment target this object (403 FEEDS_DISABLED) — a new comment and an update that re-threads an existing one alike |
534535
| **activities** | `boolean` | optional (default: `true`) | Record activity timeline (sys_activity mirror of CRUD). Default on; explicit false stops mirroring and hides the timeline |
535536
| **clone** | `boolean` | optional (default: `true`) | Allow record deep cloning |
537+
| **approvalsVisibleToReaders** | `boolean` | optional (default: `false`) | Opt-in: true lets a caller who can read a record of this object see that record's approval requests and full action history (comments and attachments included), read-only, on the approvals API and the generic data API alike, on a read that names the record. No approval action is offered. Default false: only the request's participants (submitter, approvers, past actors) and administrators see it |
536538

537539

538540
---

‎packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
293293
"enable.clone": {
294294
label: "Clone"
295295
},
296+
"enable.approvalsVisibleToReaders": {
297+
label: "Approvals Visible To Readers",
298+
helpText: "Readers of a record see its approval requests and history, read-only, with no approval action"
299+
},
296300
validations: {
297301
label: "Validations",
298302
helpText: "Object-level validation rules — an array of rule objects, e.g. [{ \"type\": \"script\", \"name\": \"amount_positive\", \"condition\": \"amount > 0\", \"message\": \"Amount must be positive\" }]. State-machine transition tables are declared here too (ADR-0020)"

‎packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
293293
"enable.clone": {
294294
label: "Clonación"
295295
},
296+
"enable.approvalsVisibleToReaders": {
297+
label: "Aprobaciones visibles para lectores",
298+
helpText: "Quien puede leer un registro ve sus solicitudes de aprobación y su historial, en solo lectura y sin ninguna acción de aprobación"
299+
},
296300
validations: {
297301
label: "Validaciones",
298302
helpText: "Reglas de validación a nivel de objeto — un array de objetos de regla, p. ej. [{ \"type\": \"script\", \"name\": \"amount_positive\", \"condition\": \"amount > 0\", \"message\": \"Amount must be positive\" }]. Las tablas de transición de máquinas de estado también se declaran aquí (ADR-0020)"

‎packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
293293
"enable.clone": {
294294
label: "クローン"
295295
},
296+
"enable.approvalsVisibleToReaders": {
297+
label: "閲覧者に承認を表示",
298+
helpText: "レコードを閲覧できるユーザーが、その承認リクエストと履歴を読み取り専用で参照できます。承認アクションは提供されません"
299+
},
296300
validations: {
297301
label: "検証ルール",
298302
helpText: "オブジェクトレベルの検証ルール — ルールオブジェクトの配列。例: [{ \"type\": \"script\", \"name\": \"amount_positive\", \"condition\": \"amount > 0\", \"message\": \"Amount must be positive\" }]。ステートマシンの遷移テーブルもここで宣言します(ADR-0020)"

‎packages/platform-objects/src/apps/translations/object-collapsed-sections-echo-decisions.test.ts‎

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,24 @@ const DECISIONS: readonly Decision[] = [
313313
reason:
314314
'⛔ NO AUTHORED TWIN FOR "clone" OR "cloning" IN EITHER CATALOG — stated, not borrowed. The nearest thing either catalog holds is object.fields["fields.unique"].helpText ("Disallow duplicate values") 不允许重复值 / 重複値を許可しない / No permite valores duplicados, and that 重复 / 重複 / duplicado is the "repeated value" sense, ⛔ not the "make a copy of this record" sense the schema means ("Allow record deep cloning"). ⇒ composed: 克隆 / クローン / Clonación. ⚠️ es takes the NOUN (Clonación) rather than the infinitive, following its own siblings on this panel, which are nominal or adjectival throughout (Archivos, Actividades, Buscable, Seguimiento de historial) — and unlike enable.label above, which copies a twin that is itself an action label.',
315315
},
316+
{
317+
section: 'capabilities',
318+
path: 'enable.approvalsVisibleToReaders',
319+
prop: 'label',
320+
en: 'Approvals Visible To Readers',
321+
verdict: ALL_TRANSLATE,
322+
reason:
323+
'ARRIVED WITH THE FLAG ITSELF (the per-object opt-in for the read-only record-reader approval tier), so all three leaves were authored by the card that added the row, never left as fills. The English is the extractor humanize of the key (objectForm declares no label, asserted by the third leg), decided against the CONCEPT: who sees what. "Approvals" takes the approvals plugin\'s own authored head noun, sys_approval_request.label 审批请求 / 承認リクエスト / Solicitud de aprobación ⇒ 审批 / 承認 / Aprobaciones. "Readers" takes the reader twin on the view panel, the visibility section description ("who can see it") 谁可以查看 / 閲覧可能者 / quién puede verla ⇒ 读者 / 閲覧者 / lectores. ⇒ 审批对读者可见 / 閲覧者に承認を表示 / Aprobaciones visibles para lectores. ⛔ NOT 只读 / 読み取り専用 / Solo lectura in the label: that is the read-only BOUNDARY, which the helpText below states, and this catalog spends those words on the readonly field row.',
324+
},
325+
{
326+
section: 'capabilities',
327+
path: 'enable.approvalsVisibleToReaders',
328+
prop: 'helpText',
329+
en: 'Readers of a record see its approval requests and history, read-only, with no approval action',
330+
verdict: ALL_TRANSLATE,
331+
reason:
332+
'EVERY NOUN HAS AN AUTHORED TWIN and the row names each, because this sentence states what a security-relevant switch grants and a loose word would widen it. "approval requests": sys_approval_request.pluralLabel 审批请求 / 承認リクエスト / Solicitudes de aprobación. "approval action": sys_approval_action.pluralLabel 审批动作 / 承認アクション / Acciones de aprobación, so the leaf names the same rows the tier refuses to act on. "read-only": fields.readonly.label 只读 / 読み取り専用 / Solo lectura. "history": the 历史 / 履歴 / historial of enable.trackHistory\'s twin. "can read a record": 读取 / 閲覧 / leer, the record-read the tier anchors on. ⇒ 能读取记录的用户可只读查看该记录的审批请求与历史,不提供任何审批动作 / レコードを閲覧できるユーザーが、その承認リクエストと履歴を読み取り専用で参照できます。承認アクションは提供されません / Quien puede leer un registro ve sus solicitudes de aprobación y su historial, en solo lectura y sin ninguna acción de aprobación. ⚠️ zh and ja name the grantee as the user who can read the record rather than "readers", so the sentence cannot be read as granting anything to a reader of the approval tables themselves.',
333+
},
316334
{
317335
section: 'advanced',
318336
path: 'validations',
@@ -487,9 +505,10 @@ const MINIMAL_OBJECT = { name: 'acct', label: 'Acct', fields: { id: { type: 'tex
487505

488506
describe('#19403 round 8 — the ledger itself (controls before verdicts)', () => {
489507
it('decides every string leaf of the two keys this round takes, and nothing else', () => {
490-
// Lit — the ledger is the size it claims: 9 form rows, 11 leaves, three
491-
// locales, 33 decisions.
492-
expect(DECISIONS.length).toBe(11);
508+
// Lit — the ledger is the size it claims: 10 form rows, 13 leaves, three
509+
// locales, 39 decisions (the opt-in flag's row brought one form row, two
510+
// leaves and six decisions).
511+
expect(DECISIONS.length).toBe(13);
493512
expect(new Set(DECISIONS.map((d) => d.path))).toEqual(
494513
new Set([
495514
'enable',
@@ -500,12 +519,13 @@ describe('#19403 round 8 — the ledger itself (controls before verdicts)', () =
500519
'enable.feeds',
501520
'enable.activities',
502521
'enable.clone',
522+
'enable.approvalsVisibleToReaders',
503523
'validations',
504524
]),
505525
);
506-
expect(DECISIONS.filter((d) => d.prop === 'label').length).toBe(9);
507-
expect(DECISIONS.filter((d) => d.prop === 'helpText').length).toBe(2);
508-
expect(DECISIONS.flatMap((d) => Object.keys(d.verdict)).length).toBe(33);
526+
expect(DECISIONS.filter((d) => d.prop === 'label').length).toBe(10);
527+
expect(DECISIONS.filter((d) => d.prop === 'helpText').length).toBe(3);
528+
expect(DECISIONS.flatMap((d) => Object.keys(d.verdict)).length).toBe(39);
509529
for (const d of DECISIONS) {
510530
expect(Object.keys(d.verdict).sort(), `${idOf(d)} names every translated locale`).toEqual([
511531
'es-ES',
@@ -685,7 +705,7 @@ describe('#19403 round 8 — ADR-0020 and the validations schema, asserted AT TH
685705
// The class-(c) question, answered at the schema rather than assumed: could
686706
// translating these labels produce metadata the runtime rejects? No — the
687707
// labels name keys whose values are booleans, and the block is strict.
688-
for (const key of ['trackHistory', 'searchable', 'apiEnabled', 'files', 'feeds', 'activities', 'clone']) {
708+
for (const key of ['trackHistory', 'searchable', 'apiEnabled', 'files', 'feeds', 'activities', 'clone', 'approvalsVisibleToReaders']) {
689709
expect(ObjectCapabilities.safeParse({ [key]: true }).success, `${key} is a declared capability`).toBe(true);
690710
expect(
691711
ObjectCapabilities.safeParse({ [key]: 'Clone' }).success,
@@ -918,9 +938,13 @@ describe('#19403 round 8 — the population, DERIVED from the form and a shape',
918938
// a label and a help text each, thirty-six leaves, all hundred and eight
919939
// translated leaves authored by the same flight. `advanced` reads 60 → 96;
920940
// `capabilities` is untouched.
921-
expect(PANEL_LEAVES.length).toBe(105);
941+
// 107 since the record-reader approval opt-in gave `capabilities` its
942+
// `enable.approvalsVisibleToReaders` toggle — a label and a help text, two
943+
// leaves, all six translated leaves authored by the same card, decided in
944+
// the two rows above. `capabilities` reads 9 → 11; `advanced` is untouched.
945+
expect(PANEL_LEAVES.length).toBe(107);
922946
expect(PANEL_LEAVES.every((l) => l.prop === 'label' || l.prop === 'helpText')).toBe(true);
923-
expect(PANEL_LEAVES.filter((l) => l.section === 'capabilities').length).toBe(9);
947+
expect(PANEL_LEAVES.filter((l) => l.section === 'capabilities').length).toBe(11);
924948
expect(PANEL_LEAVES.filter((l) => l.section === 'advanced').length).toBe(96);
925949
});
926950

@@ -1063,14 +1087,14 @@ describe('#19403 round 8 — the population, DERIVED from the form and a shape',
10631087
expect(flagged.length).toBe(PANEL_LEAVES.length);
10641088
});
10651089

1066-
it('⭐ the capability block is now DONE — zero of its 9 leaves echoes in any locale', () => {
1090+
it('⭐ the capability block is now DONE — zero of its 11 leaves echoes in any locale', () => {
10671091
const capability = PANEL_LEAVES.filter((l) => l.section === 'capabilities');
10681092
const echoing = capability.filter((l) =>
10691093
TRANSLATED_LOCALES.some(([, forms]) => catalogLeaf(forms, l.path, l.prop) === l.en),
10701094
);
10711095
expect(echoing.map((l) => `${l.path}.${l.prop}`)).toEqual([]);
10721096
// Lit — and it really walked the section, which a zero alone would not show.
1073-
expect(capability.length).toBe(9);
1097+
expect(capability.length).toBe(11);
10741098
});
10751099
});
10761100

‎packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,10 @@ describe('#19403 round 10 — the verdicts, on the live bundles', () => {
11771177
// 667 since the field form offers the deadline pair on date and datetime
11781178
// fields, `dueLike` and `settledWhen` — two new row labels, authored in
11791179
// all three locales.
1180-
expect(translated.length, `${locale} positive control`).toBe(667);
1180+
// 668 since the object form offers `enable.approvalsVisibleToReaders`,
1181+
// the per-object opt-in for the read-only record-reader approval tier —
1182+
// one new row label, authored in all three locales.
1183+
expect(translated.length, `${locale} positive control`).toBe(668);
11811184
}
11821185
// ⭐ DARK — the blindness, executable. On a synthetic two-locale catalog the
11831186
// all-three predicate returns 0 while the per-locale one returns 1, so the

‎packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
293293
"enable.clone": {
294294
label: "克隆"
295295
},
296+
"enable.approvalsVisibleToReaders": {
297+
label: "审批对读者可见",
298+
helpText: "能读取记录的用户可只读查看该记录的审批请求与历史,不提供任何审批动作"
299+
},
296300
validations: {
297301
label: "校验规则",
298302
helpText: "对象级校验规则——由规则对象组成的数组,例如 [{ \"type\": \"script\", \"name\": \"amount_positive\", \"condition\": \"amount > 0\", \"message\": \"Amount must be positive\" }]。状态机转移表也在此声明(ADR-0020)"

0 commit comments

Comments
 (0)