Skip to content

Commit 69712d5

Browse files
spec: the flows.approvals translation rows read live, and ApprovalRequestRow's label docblocks name the reader's-locale label (#25103)
Closes #24979 Clause-②: no (ledger and docblock text only: no shape, key, default, accepted value or export moves) ## What The follow-up owed by #24863's landing (PR #24977, commit ba69b16). plugin-approvals now serves an approval request's flow, step and object labels in the reader's locale (`packages/plugins/plugin-approvals/src/reader-locale-labels.ts`). The spec side still said otherwise in two places. This PR fixes both: 1. **`packages/spec/liveness/translation.json`**: `flows.approvals` and its one drilled child `flows.approvals.label` flip `planned` to `live`. This is the step the row's own planned note asked for ("Flip to `live` on the commit that cites that reader"). Both rows now cite the reader by `file#symbol` anchors. The child also cites its producer chain. The generated count shard `liveness/state-counts/translation.md` was regenerated with `gen:liveness-counts` (live 29 to 30, planned 1 to 0). The `translation` Notes cell in `liveness/README.md` gains one sentence for the flip, as the gate's stale-count message asks. 2. **`packages/spec/src/contracts/approval-service.ts`**: the `ApprovalRequestRow` docblocks for `process_label`, `step_label`, `current_step_label` and `object_label` now say what is served. Each is the reader's-locale label, falling back to the stored snapshot (for `object_label`, the object's schema label). The stored columns are never rewritten by a read. Text only: no type, key, default, accepted value or export moves. The docblocks ship in `dist/contracts/index.d.ts`, so this carries a `@objectstack/spec` `patch` changeset (`.changeset/24979-approvals-label-live.md`). ## Each docblock statement, verified against plugin-approvals at origin/main a39389e None of these files is touched by this diff, so the line numbers hold at this PR's head too. | Docblock statement | Where plugin-approvals does it | |---|---| | The flow label is picked from `flows.FLOW.label`, the step label from `flows.FLOW.approvals.NODE.label`, the object label from `objects.OBJECT.label` | `reader-locale-labels.ts:73` `flowLabelKey`, `:78` `approvalStepLabelKey`, `:153` `objectLabelKey` | | Picked through the i18n service in the read's `ExecutionContext.locale`, negotiated by `resolveBundleLocale` | `reader-locale-labels.ts:128-155` `readerLabelsFor` (`:134` negotiation, `:140` `service.t(key, locale)`); `approval-service.ts:8250-8259` `readerLabels`; `approvals-plugin.ts:273` wires `i18nServiceSource` | | No locale, no i18n service or no pack entry: the snapshot is served | `reader-locale-labels.ts:132-133` (no locale or no service gives `undefined`), `:142` (a miss echoes the key and picks nothing), `:178`, `:181`, `:188` (overlay only when picked) | | Picked on `listRequests`, `getRequest` and a write's echoed request | `approval-service.ts:8229-8230` (`listRequests`), `:8371-8372` (`loadRequest`); `getRequest` (`:8337-8338`) and `readBackRequest` (`:8326`, every decision or write echo) go through `loadRequest` | | `step_label` and `current_step_label` are always the same value | `approval-service.ts:1270`, `:1275` (both from one `requestDisplayLabels` value); `reader-locale-labels.ts:182-183` (both overlaid together) | | Fallback is the authored label as snapshotted at open, else the humanized machine name | `display-labels.ts:79-80` `requestDisplayLabels` (column, else the node-config snapshot, else the humanized name) | | The stored column is never rewritten; the generic data door and list views serve the snapshot | `reader-locale-labels.ts:56-59` (header, "What stays"); the overlay mutates only the served row object (`:174-189`) | | `object_label` falls back to the object's schema label | `approval-service.ts:7429-7433` `objectLabelOf` (`schema.label`), set at `:7378` and `:7574-7575` before the overlay | ## Deferred, not in this PR The optional `flowLabelKey` / `flowApprovalLabelKey` helper beside `flowRefusalMessageKey` (issue body, "Optional") is not added. A new `@objectstack/spec` export is a widening, which the claim's Clause-② `no` excludes. `reader-locale-labels.ts` keeps spelling both keys locally, held to `TranslationDataSchema` by `reader-locale-labels.test.ts`. ## Verification (head da0364f) All runs below were taken on this head (no commit after them). The spec build and tests ran under the shared verify lock on a shared box. - `pnpm --filter @objectstack/spec build`: `VERDICT command-exit 0`. The edited docblocks are in `dist/contracts/index.d.ts` (one hit for the new text), so the published artifact moves and the changeset is a `patch`. - `pnpm --filter @objectstack/spec typecheck` (tsc, scripts and test typecheck) followed by `vitest run`: `Test Files 778 passed (778)`, `Tests 22683 passed, 1 todo`; `VERDICT command-exit 0`. - `pnpm --filter @objectstack/spec test:repo`: `Test Files 72 passed (72)`, `Tests 1237 passed (1237)`; `VERDICT command-exit 0`. - `pnpm --filter @objectstack/spec check:liveness`: red before regeneration, as it should be (`state-counts/translation.md is STALE`: live 29 to 30, planned 1 to 0). After `gen:liveness-counts` it is green: every `path#symbol` anchor resolves (976 of 976), key-mention 747 anchored plus 1 exempt, and `state-counts/ is current`. - `pnpm --filter @objectstack/spec check:generated`: `All 14 generated artifacts are up to date`, against the dist this branch built. - `node scripts/pm/dispatch-gates.mjs --commands` derived 84 families for this diff, and all 84 were run. 81 exited 0. `--ran` with recorded exit codes: `84 derived, 81 run, 3 NOT-MEASURED, 0 UNRUN`. Three were NOT MEASURED. Each exited 3 because a build it needs was missing, so it checked nothing: `@objectstack/lint check:doc-formula-expressions` (needs formula and lint dist), `check:dual-build-cjs-loads` (needs every package's dist), and `check:lean-entry-closure` (needs objectql dist). They are left to CI. The changeset gates (`check-changeset-no-major`, `check-adr-0087-registration`, `check-empty-changeset`, `check:changeset-gate-self-tests`), `check:nul-bytes`, `check-spec-docblock-symbol-anchors`, `check:api-surface` and `check:docs` are among the 81 that exited 0. - `pnpm lint` (repo-wide) was not run locally. It belongs to CI. ## Acceptance notes - No open PR touches any of the four changed files under `packages/spec` (all 14 open PRs' file lists scanned before opening). - `flow_steps[].label` has no docblock that names the authored label (its member docblock is silent on the label's source), so it is left unchanged, although `attachFlowSteps` (`approval-service.ts:8501`) names steps by the same pick. --- _Generated by [Claude Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2e242bb commit 69712d5

5 files changed

Lines changed: 54 additions & 15 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `ApprovalRequestRow`'s display labels are documented as the reader's-locale label
6+
7+
Clause-②: no (ledger and docblock text only: no shape, key, default, accepted value or export moves)
8+
9+
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.<flow>.label`, `flows.<flow>.approvals.<node_id>.label` or `objects.<object>.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.

‎packages/spec/liveness/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,7 @@ marker where the Notes cell goes, never a guess at what belongs there.
980980
| 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 |
981981
| 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. |
982982
| 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 |
983-
| 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.<o>._views.<v>.bulkActions` and `objects.<o>._validations.<rule>.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. |
983+
| 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.<o>._views.<v>.bulkActions` and `objects.<o>._validations.<rule>.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.<flow>.approvals.<node_id>.label` in the reader's locale, falling back to the stored snapshot, so the type's planned set is empty. |
984984
| 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 |
985985
| 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 |
986986
| 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 |

‎packages/spec/liveness/state-counts/translation.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them.
1212

1313
| Type | live | exp | elsewhere | dead | planned | classified |
1414
|---|---|---|---|---|---|---|
15-
| `translation` | 29 | 0 | 0 | 0 | 1 | 30 |
15+
| `translation` | 30 | 0 | 0 | 0 | 0 | 30 |

0 commit comments

Comments
 (0)