diff --git a/.changeset/24979-approvals-label-live.md b/.changeset/24979-approvals-label-live.md new file mode 100644 index 00000000000..ce2c487ba8e --- /dev/null +++ b/.changeset/24979-approvals-label-live.md @@ -0,0 +1,9 @@ +--- +'@objectstack/spec': patch +--- + +docs(spec): `ApprovalRequestRow`'s display labels are documented as the reader's-locale label + +Clause-②: no (ledger and docblock text only: no shape, key, default, accepted value or export moves) + +The `ApprovalRequestRow` docblocks for `process_label`, `step_label`, `current_step_label` and `object_label` named the authored or schema label. Since plugin-approvals serves those members in the reader's locale, they now say so: the served value is the translation picked from `flows..label`, `flows..approvals..label` or `objects..label` in the read's `ExecutionContext.locale`, falling back to the stored snapshot (or the object's schema label). The stored `sys_approval_request` columns are unchanged. The `flows.approvals` translation liveness rows read `live`. No type, key or export changes. diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 1dd14dc1ce7..fbfdcc6d3d2 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -980,7 +980,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | mapping | seeded 2026-08-01 (#4488) at 8/11 live; **0 dead since #4509** retired the three that were not. The import half (#2611) is loudly enforced — unsupported transforms/formats are 400s, `mode`/`upsertKey` default the request, the wizard picker renders `label`. RETIRED 17.0.0: `extractQuery` (authorWarn — "for export only" promised an export path no exporter implements) + `errorPolicy`/`batchSize`, which were dead AND **unwarnable** (schema defaults materialize at parse, so presence ≠ authored — `_authorWarnSkipped`, the non-boolean instance of the default(true) rule). That unwarnability is why they went out in the 17.0.0 window rather than after a deprecation cycle: removal was the only channel that could ever reach the author. Rows DELETED, not tombstoned — MappingSchema is strict, so the keys left the walked shape. `connectorSource` (2026-09-30) is this type's first `planned` row: the pull binding the connector-attached sync family MOVED here when the connector's `syncConfig` / `fieldMappings` retired to tombstones, seeded contract-first with `authorWarn` (its seven drilled rows — five children and `watermark`'s two — `planned` with it) because the pull executor is the next stage. **Live since 2026-10-01 (#20919)**: the connector sync executor reads every key. The container's `authorWarn` left with that re-grade (#21127) — kept at first, it made the author-side lint throw, so `os validate` / `os lint` exited 1 on every stack that authored the binding; the caveat it carried (nothing schedules a pull until the `job` stage lands) is on the key's description | | picklist | seeded 2026-09-30 (#19518) with every key `planned`, ahead of its runtime reader by ruling; **live since the runtime layer (#19519)**: the ObjectQL registry merges `picklistExtensions` into the list (additive only, a repeated value refused) and resolves it onto every field that names it, the write door judges against that set, and a field naming an undeclared list fails the boot. `field.picklist` left `authorWarn` with it. Display keys (`label`, `description`, `options.color`) are graded by their served form. | | seed | seeded 2026-08-01 (#4488). Fully live via SeedLoaderService on both doors (boot/per-org replay + runtime-draft publish). `records` is the z.record walk boundary: the keys an author writes are the target object's fields, governed by that object's own definitions — recorded in the entry, not silently skipped | -| translation | seeded 2026-08-01 (#4488) — after fixing the walker: the registered schema is a z.preprocess pipe (#3778 retired-dialect guard) whose transform side the unwrap always took, so the type was literally unwalkable. 11 of 12 groups live across spec resolvers, REST localization, objectui client resolvers and plugin-audit (whose composed-key `t()` calls make `messages` easy to mis-verify as dead) — `flows` was the one that was not, and was `planned` at seeding. **#14253** added the twelfth, `datasets`, seeded LIVE and DRILLED (label / description / dimensions / measures) with its reader in the same change: `translateDataset` in the dispatch table, which is what `TRANSLATABLE_METADATA_TYPES` is derived from, so the REST boundary followed with nothing else to remember. The same change gave `objects.._views..bulkActions` and `objects.._validations..message` their first keys — both beneath the walk boundary, so neither adds a row here. Dead 1 = `validationMessages` (authorWarn) at seeding: nothing resolved it, and #3778's own legacy-key migration table steered `errors:` authors into it — a shipped false signpost, the capabilities.readOnly shape. **#4667**: `validationMessages` REMOVED (row deleted) — removed from the shared translationDataShape(), so it retired at BOTH doors at once, closing the item-only asymmetry #3778's original guard had. #3778's own `errors` guidance was rewritten in the same change: it had been steering authors INTO this dead group. ⚠️ **What that left behind is this table's own worked example of the defect it warns about** (#7377): the same commit that deleted the `validationMessages` row wrote a count column of `dead 2` beside a sentence that named exactly one dead key — and that one was the key it had just removed. The real two were `name` and `label`, which the cell never mentioned. Measured at that commit, not inferred: the ledger's dead set there is `{name, label}` and `validationMessages` is absent from `props`. The number was right and the prose was false, in the same cell, on the day it was written — which is why the counts are now generated and this cell holds prose only. **#7131** (PR #7425) resolves it: `name` and `label` re-grade `dead` → `live` under the designer-previews-count-as-consumers ruling (objectui `TranslationPreview.tsx:67` reads `label` first and falls back to `name`, both rendering at `:100`), so the dead set is empty and there is no dead-set sentence left to keep true. As on `job`, the ADR-0033 docs-shaped exemption is untouched — nothing about enforce-or-remove moved. **#19620** (ruling batch #210 item 2 letter B): `settings` row DELETED — the strict-delete route, because `TranslationItemSchema` no longer declares the key and refuses it by name (the item door now takes the per-app face, as the file door has since #15178). ⚠️ The deleted row read `live`, and that verdict was TRUE and stays true of the platform: its evidence read the SERVED tree, which the platform bundle feeds, so the deletion retires the key from the application-authored item and nothing else — the capability lives on `PlatformTranslationDataSchema`, outside this ledger. **#20296**: `flows` is now half-read. `flows.screens` re-graded `planned` → `live`: objectui's FlowRunner reads each screen's `title` and each field's `label` / `placeholder` at the `.objectui-sha` pin f8a9d0fb. `flows.label` stays `planned` because nothing reads it yet (#20318), and so does the container's `authorWarn`, whose `authorHint` now names the read half and the unread one. **#23710**: the terminal toasts `flows.successMessage` / `flows.errorMessage` (child rows added `planned` by #22507) re-graded `live`: objectui's `translatedFlowMessage` asks `translateFlow` for both at the `.objectui-sha` pin daeb77fc84dd. | +| translation | seeded 2026-08-01 (#4488) — after fixing the walker: the registered schema is a z.preprocess pipe (#3778 retired-dialect guard) whose transform side the unwrap always took, so the type was literally unwalkable. 11 of 12 groups live across spec resolvers, REST localization, objectui client resolvers and plugin-audit (whose composed-key `t()` calls make `messages` easy to mis-verify as dead) — `flows` was the one that was not, and was `planned` at seeding. **#14253** added the twelfth, `datasets`, seeded LIVE and DRILLED (label / description / dimensions / measures) with its reader in the same change: `translateDataset` in the dispatch table, which is what `TRANSLATABLE_METADATA_TYPES` is derived from, so the REST boundary followed with nothing else to remember. The same change gave `objects.._views..bulkActions` and `objects.._validations..message` their first keys — both beneath the walk boundary, so neither adds a row here. Dead 1 = `validationMessages` (authorWarn) at seeding: nothing resolved it, and #3778's own legacy-key migration table steered `errors:` authors into it — a shipped false signpost, the capabilities.readOnly shape. **#4667**: `validationMessages` REMOVED (row deleted) — removed from the shared translationDataShape(), so it retired at BOTH doors at once, closing the item-only asymmetry #3778's original guard had. #3778's own `errors` guidance was rewritten in the same change: it had been steering authors INTO this dead group. ⚠️ **What that left behind is this table's own worked example of the defect it warns about** (#7377): the same commit that deleted the `validationMessages` row wrote a count column of `dead 2` beside a sentence that named exactly one dead key — and that one was the key it had just removed. The real two were `name` and `label`, which the cell never mentioned. Measured at that commit, not inferred: the ledger's dead set there is `{name, label}` and `validationMessages` is absent from `props`. The number was right and the prose was false, in the same cell, on the day it was written — which is why the counts are now generated and this cell holds prose only. **#7131** (PR #7425) resolves it: `name` and `label` re-grade `dead` → `live` under the designer-previews-count-as-consumers ruling (objectui `TranslationPreview.tsx:67` reads `label` first and falls back to `name`, both rendering at `:100`), so the dead set is empty and there is no dead-set sentence left to keep true. As on `job`, the ADR-0033 docs-shaped exemption is untouched — nothing about enforce-or-remove moved. **#19620** (ruling batch #210 item 2 letter B): `settings` row DELETED — the strict-delete route, because `TranslationItemSchema` no longer declares the key and refuses it by name (the item door now takes the per-app face, as the file door has since #15178). ⚠️ The deleted row read `live`, and that verdict was TRUE and stays true of the platform: its evidence read the SERVED tree, which the platform bundle feeds, so the deletion retires the key from the application-authored item and nothing else — the capability lives on `PlatformTranslationDataSchema`, outside this ledger. **#20296**: `flows` is now half-read. `flows.screens` re-graded `planned` → `live`: objectui's FlowRunner reads each screen's `title` and each field's `label` / `placeholder` at the `.objectui-sha` pin f8a9d0fb. `flows.label` stays `planned` because nothing reads it yet (#20318), and so does the container's `authorWarn`, whose `authorHint` now names the read half and the unread one. **#23710**: the terminal toasts `flows.successMessage` / `flows.errorMessage` (child rows added `planned` by #22507) re-graded `live`: objectui's `translatedFlowMessage` asks `translateFlow` for both at the `.objectui-sha` pin daeb77fc84dd. **#24979**: `flows.approvals` and its one drilled key `label` (added `planned` as the spec half of #24863) re-graded `live`: plugin-approvals' `reader-locale-labels.ts` (commit ba69b168be) serves an approval request's step label from `flows..approvals..label` in the reader's locale, falling back to the stored snapshot, so the type's planned set is empty. | | qa | seeded 2026-08-10 (#6247) — **not a metadata type**: `TestSuiteSchema` is the FILE surface of the shipped `os test` command (`qa/*.test.json`), governed through the same `SPEC_ONLY_SCHEMAS` override as `query`/`webhook`/`validation`. It is in the table as the clearest worked example of a **false `dead` measurement**: #6247 reported the whole domain declared-but-inert on a grep that scanned only `*Schema` identifiers, and every consumer here reads the **type** names (`QA.TestSuite`, `QA.TestStep`, `QA.TestAction`) — so an entire execution chain (core's `TestRunner` + `HttpTestAdapter`, published via `export * as QA`, driven by a documented CLI command) read as zero consumers, and a retire ruling was issued on it before being withdrawn. The `evidenceScope` table one section up says no amount of specifier matching is sufficient for a negative claim; this is the same lesson for **identifier** matching. What was really wrong was narrower and real: the type was the contract and the schema had no `parse` site, so the CLI's `JSON.parse(content) as QA.TestSuite` cast admitted anything — ENFORCED in the same change (`TestSuiteSchema.safeParse` at the load site, pinned). The seeding recorded Dead 5 — `name`, `scenarios.name`, `scenarios.description`, `scenarios.tags` and `scenarios.requires` — and #20289 (family `qa-runner`, verdict ENFORCE) made all FIVE live: the suite `name` heads the suite in `os test`'s report and is stamped as `suiteName` on every TestResult the suite produces; `scenarios.name` is printed beside the id and carried as `scenarioName`; `scenarios.description` is carried and printed under a failed scenario; `scenarios.tags` is read by `os test --tags` (any-of, exact; deselected scenarios are counted and never passed); and `scenarios.requires` is judged by the TestRunner before a scenario's first step — `params` against the environment of the process running `os test`, `services` against the target's discovery `services` (enabled and `available`) — so an unmet entry SKIPS the scenario with its reason, counted apart and never passed. `requires` was put to the maintainer first rather than guessed (ruled B): over HTTP no server surface lists the loaded plugins, so `requires.plugins` is a `retiredKey()` tombstone inside the block whose prescription names `requires.services`. It does not carry `authorWarn` and the omission is deliberate (`_authorWarnSkipped`): the lint walks stack **collections**, a QA suite is a loose file in no stack, so a warn flag here would emit nothing — a silent no-op inside the mechanism built to catch silent no-ops | | validation | seeded 2026-08-01 (#4488). The ADR-0020 carrier: the evaluator honors active/events/priority/severity/type/condition/message (the zod header's "only reads type/condition/…" prose is STALE — trust the ledger). Dead 3 = label/description/tags, declared governance metadata, kept unmarked. Union walk boundary recorded: only base + `script` keys walked; per-variant keys are governed by the evaluator's tests, not ledger rows. **No longer a registered metadata kind** — #4509 retired it under ADR-0088 (a standalone rule had no object-binding key and every variant is `.strict()`, so it bound to nothing and gated no write; a state machine authored that way saved cleanly and did nothing). The rule VOCABULARY is untouched and fully live via `object.validations[]`, so the ledger keeps governing it through the gate's spec-only override, alongside `webhook` and `query`. The contrast with the two bridges in the same batch is the point: enforce-or-remove picked ENFORCE where the feature existed and only the wiring was missing, and REMOVE where the shape itself could not carry the feature | | api | seeded 2026-08-04 (#5271, part of #5206; PR #5312) — **not a metadata type until that same change made it one**, which is the row's point: governance and registration landed together, the treatment `datasource` did not get (#4487) and paid for with six inert keys found by hand. What #5206 measured before the fix: `api` was in neither `DEFAULT_METADATA_TYPE_REGISTRY` nor `BUILTIN_METADATA_TYPE_SCHEMAS`, so `saveMetaItem`'s `resolveOverlaySchema('api', …)` → `getMetadataTypeSchema('api')` returned `undefined` and took its own documented branch — an unregistered type is stored **unvalidated** — while `getMetaTypes()` could not enumerate the type at all, so Studio rendered neither list nor form. That issue names the shape precisely and it is the inverse of this ledger's usual one: **enforced but undeclared** (the matcher was already indexing these entries, #5089), where `dead` is declared-but-unenforced. The seeding pass classified 27 keys — live 25 / planned 2 / dead 0 — each cited `file:line` at the consumer layer that reads it: the MATCHER (`packages/metadata/src/endpoint-matcher.ts`) indexes `name`/`path`/`method`; the EXECUTOR (`packages/runtime/src/endpoint-executor.ts`) dispatches on `type` and reads `target`/`objectParams`; the POLICY chain (`packages/runtime/src/endpoint-policy.ts` + `security/inbound-rate-limit.ts`) enforces `authRequired`/`rateLimit`/`cacheTtl`; the MAPPING layer (`packages/runtime/src/api-mapping.ts`) applies `inputMapping`/`outputMapping`; and OpenAPI enrichment (`packages/rest/src/openapi-endpoints.ts`) emits `summary`/`description`. Timing was the reason it was cheap: #5040's E-series had built every one of those consumers and all of it was on main, so each key had a real evidence path rather than a promise. **Planned 2 = `inputMapping.transform` + `outputMapping.transform`, and `planned` rather than `dead` is load-bearing**: `dead` here means parsed with no consumer — a silent no-op — and these are the opposite, parsed and then LOUDLY REFUSED at publish (`endpoint-publish-gate.ts` mappingGate) and again at runtime, because no transformation-function registry exists anywhere in the platform. An author who writes one is told so and told what to do instead, so there is nothing for enforce-or-remove to chase; they stay in the vocabulary because admitting them needs a function registry **and** a sandbox ruling (#5040 §3.4), which is a design decision, not a key to quietly delete. Zero dead | diff --git a/packages/spec/liveness/state-counts/translation.md b/packages/spec/liveness/state-counts/translation.md index 151faa42c81..c60db53046e 100644 --- a/packages/spec/liveness/state-counts/translation.md +++ b/packages/spec/liveness/state-counts/translation.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `translation` | 29 | 0 | 0 | 0 | 1 | 30 | +| `translation` | 30 | 0 | 0 | 0 | 0 | 30 | diff --git a/packages/spec/liveness/translation.json b/packages/spec/liveness/translation.json index c619a941121..c0898b04fc5 100644 --- a/packages/spec/liveness/translation.json +++ b/packages/spec/liveness/translation.json @@ -167,15 +167,19 @@ "note": "[#22450] Seeded `live` together with its reader: the key and the engine read that makes it true land in one change (Prime Directive 10). The run stores the refusal already rendered (`sys_automation_run.refusal_message`, `AutomationResult.refusalMessage`), so no overlay on the served string could translate it; the engine picks the translated TEMPLATE before it fills the holes, the validation-message precedent (`objectValidationMessageKey`, read by objectql's rule evaluator through the same `i18n` channel). A resumed leg renders in the locale the run was started in, and a run no person started (record-change, schedule) carries no locale and stores the authored message. The translation is a text slot like the source: `TranslationDataSchema` refuses a single-brace `{token}` in it through `textSlotTemplateRefusal`, and `objectstack validate` (`validate-translation-references.ts`) refuses a key over an unknown flow, an unknown node, or a node that is not an `end` declaring `outcome: 'refused'`. No objectui reader is involved: the console draws the served `refusalMessage` verbatim. DRILLED to its one key, `message`, so no blanket verdict covers an unclassified child: the entry's face is closed (`.strict()`), and `message` carries the reader and producer evidence." }, "approvals": { - "status": "planned", - "verifiedAt": "2026-10-20", + "status": "live", + "verifiedAt": "2026-10-21", "evidenceScope": "in-repo", - "note": "PLANNED: the spec half of a contract-first split. The face declares `flows..approvals..label`, the step label of an `approval` node, and `objectstack validate` (packages/lint/src/validate-translation-references.ts#validateTranslationReferences) refuses a key over an unknown flow, an unknown node, or a node that is not an `approval`. No reader renders it yet: measured at this commit's base 81e6edd463, packages/plugins/plugin-approvals/src/approval-service.ts copies the node's authored `label` onto `sys_approval_request.current_step_label` when the request opens, and nothing re-derives it in the reader's locale. Carrier: the `plugin-approvals` reader half of #24863. Flip to `live` on the commit that cites that reader. DRILLED to its one key, `label`, so no blanket verdict covers an unclassified child: the entry's face is closed (`.strict()`).", + "evidence": "packages/plugins/plugin-approvals/src/reader-locale-labels.ts#localizeRequestLabels (the one reader of the group: each `approvals.` entry is read for its `label`, the drilled child below)", + "note": "FLIPPED planned → live 2026-10-21 (#24979), on the commit that cites the reader, as this row's planned note prescribed. The reader is the `plugin-approvals` half of #24863, landed in commit ba69b168be (`git merge-base --is-ancestor ba69b168be origin/main` answers yes at a39389eb51): the approvals door serves a request's step label in the READER's locale, picked from `flows..approvals..label` through the deployment's `i18n` service when the row is read; see the `label` row for the chain. It was `planned` (verified 2026-10-20) as the spec half of a contract-first split: the face declared the key, and `objectstack validate` (packages/lint/src/validate-translation-references.ts#validateTranslationReferences) refuses a key over an unknown flow, an unknown node, or a node that is not an `approval`, while plugin-approvals still served only the authored snapshot. DRILLED to its one key, `label`, so no blanket verdict covers an unclassified child: the entry's face is closed (`.strict()`).", "children": { "label": { - "status": "planned", - "verifiedAt": "2026-10-20", - "note": "Planned with its container `flows.approvals` (see that row): overlays the `approval` node's own `FlowNode.label`, the step name an approval request shows; read by no consumer yet." + "status": "live", + "verifiedAt": "2026-10-21", + "evidenceScope": "in-repo", + "evidence": "packages/plugins/plugin-approvals/src/reader-locale-labels.ts#approvalStepLabelKey (spells `flows..approvals..label`, `` the request's `process_name` without its `flow:` prefix and `` its `current_step`); packages/plugins/plugin-approvals/src/reader-locale-labels.ts#readerLabelsFor (asks `II18nService.t()` in the reader's `ExecutionContext.locale`, negotiated by `resolveBundleLocale`; a miss echoes the key and picks nothing); packages/plugins/plugin-approvals/src/reader-locale-labels.ts#localizeRequestLabels (overlays the served `step_label` and `current_step_label` together with the picked label, and keeps the stored snapshot when nothing is picked); packages/plugins/plugin-approvals/src/approval-service.ts#readerLabels (asked by `listRequests`, the inbox and the record's approval panel, and by `loadRequest`, the get and every write's echo); packages/plugins/plugin-approvals/src/approval-service.ts#attachFlowSteps (names each `flow_steps` approval step by the same pick, else the node's authored label)", + "producer": "packages/runtime/src/app-plugin.ts#loadTranslations (hands each bundle's locale data to the i18n service whole, `flows` included); packages/plugins/plugin-approvals/src/approvals-plugin.ts#i18nServiceSource (the approvals service's lazy reader of that service); packages/plugins/plugin-approvals/src/approval-service.ts#openNodeRequest (snapshots the node's authored `label` onto `sys_approval_request.current_step_label` when the request opens: the text a reader is served when nothing is picked)", + "note": "Overlays the `approval` node's own `FlowNode.label`, the step name an approval request shows. FLIPPED planned → live 2026-10-21 with its container `flows.approvals` (see that row), reader landed in ba69b168be. A read, never a write: the stored `current_step_label` stays the authored snapshot (the audit record, and what the generic data door and list views serve). Nothing is picked when the read carries no locale (a system context, the token-bearing action page), when no `i18n` service is registered, when the pack holds no entry, or when the service throws. Pinned end to end by packages/plugins/plugin-approvals/src/reader-locale-display-labels.integration.test.ts, and the key spelling is held to `TranslationDataSchema` by packages/plugins/plugin-approvals/src/reader-locale-labels.test.ts." } } } diff --git a/packages/spec/src/contracts/approval-service.ts b/packages/spec/src/contracts/approval-service.ts index e1e2e9274c2..8b54e1acd40 100644 --- a/packages/spec/src/contracts/approval-service.ts +++ b/packages/spec/src/contracts/approval-service.ts @@ -217,15 +217,36 @@ export interface ApprovalRequestRow { */ submitted_at?: string; // ── Display enrichment (inbox-facing; resolved by the service) ───── - /** Human label of the originating flow (e.g. "Project Budget Approval"). */ + /** + * Human label of the originating flow (e.g. "Project Budget Approval"), in + * the READER's locale: the translation `flows..label` picked through + * the deployment's i18n service in the read's `ExecutionContext.locale`, + * falling back to the stored snapshot, the flow's authored label as it stood + * when the request opened (`sys_approval_request.process_label`, else the + * humanized flow name when no label was authored). A read with no + * locale, with no i18n service, or whose pack holds no entry is served the + * snapshot. Picked on the rows this service returns (`listRequests`, + * `getRequest`, and the request a decision or other write echoes); the + * stored column is never rewritten, so the generic data door and the list + * views over `sys_approval_request` serve the snapshot. + */ process_label?: string; - /** Human label of the approval step / node (e.g. "Manager Review"). */ + /** + * Human label of the approval step / node (e.g. "Manager Review"), in the + * READER's locale: the translation `flows..approvals..label` + * picked the way {@link ApprovalRequestRow.process_label} is, falling back + * to the stored snapshot, the Approval node's authored label as it stood + * when the request reached the step (`sys_approval_request.current_step_label`). + * Always the same value as {@link ApprovalRequestRow.current_step_label}. + */ step_label?: string; /** - * The step's label under the declared column's own name: - * `sys_approval_request.current_step_label`, the Approval node's authored - * label as it stood when the request reached the step (the value - * {@link ApprovalRequestRow.step_label} carries). Served so a page bound to + * The step's label under the declared column's own name, + * `sys_approval_request.current_step_label`: the value + * {@link ApprovalRequestRow.step_label} carries, in the reader's locale and + * falling back to the column's stored snapshot (the Approval node's + * authored label as it stood when the request reached the step). The column + * itself is never rewritten by a read. Served so a page bound to * `sys_approval_request` can list the step by its label. Such a page names * the object's declared fields, and `step_label` is not one, so without this * member it can list only `current_step`, the node's machine id. @@ -239,7 +260,12 @@ export interface ApprovalRequestRow { record_title?: string; /** Display name of the submitter (`sys_user.name`), when resolvable. */ submitter_name?: string; - /** Schema label of the target object (e.g. "Project" for `showcase_project`). */ + /** + * Label of the target object (e.g. "Project" for `showcase_project`), in the + * READER's locale: the translation `objects..label` picked the way + * {@link ApprovalRequestRow.process_label} is, falling back to the object's + * schema label. + */ object_label?: string; /** * Display names for user-id entries in `pending_approvers`