diff --git a/.changeset/20296-understated-planned-rows.md b/.changeset/20296-understated-planned-rows.md new file mode 100644 index 00000000000..201e9fa4458 --- /dev/null +++ b/.changeset/20296-understated-planned-rows.md @@ -0,0 +1,13 @@ +--- +"@objectstack/spec": patch +--- + +Liveness ledger: three rows that were graded `planned` are now `live`, because objectui reads them at the `.objectui-sha` pin this repo builds against. The prose these flips made false is corrected too. Ledger data, one author hint and comments only. ⛔ No schema, parse, `.describe()` or accept-set change. + +The ledgers ship inside this package (`files[]` includes `liveness`), and `@objectstack/lint` reads them, so the rows an upgrading reader or tool consults are these. `@objectstack/lint` warns on a `planned` row only when the row sets `authorWarn`, and none of the three flipped rows does, so the set of warnings does not change. One warning's hint text does change (see `flows` below). + +- **`action.onSuccess.navigate` and `action.onSuccess.openIn` are `live`.** The console's action runner performs the declared post-success hop after an `api` or `script` action succeeds. It interpolates `navigate` with the `${param.*}`, `${ctx.*}` and `${result.*}` scopes, refuses a URL that is neither http(s) nor relative, and opens a new tab only on `openIn: 'newTab'`, so the materialized `'self'` default has one source of truth. The action renderers and the declared-actions bar forward the block to the runner, and the console wires the runner's navigation to its router. Both rows had been `planned` since the contract landed spec-first ahead of this reader. +- **`translation.flows.screens` is `live`.** The console's screen-flow runner draws each screen's heading and each field's `label` / `placeholder` from `flows.FLOW.screens.NODE_ID` in the active language, and falls back to the authored string key by key. The bundle reaches it through the translations route and the console's language loader. +- **`translation.flows.label` stays `planned`, and the `flows` group keeps its `authorWarn`.** Nothing reads the flow's own label yet. So `os lint` still warns when a bundle authors `flows`, and the i18n coverage demand for `flows.*` stays held back. The warning's hint used to say no shipped runner reads the group. It now says the runner reads `screens`, and that only the flow `label` is stored and never shown. +- **Prose corrected.** The `onSuccess` JSDoc in `ui/action.zod.ts` now names the console reader (`ActionRunner`'s `navigateOnSuccess`). The `flows` JSDoc in `system/translation.zod.ts` and the `translateFlow` docblock in `system/i18n-resolver.ts` now say `screens` is read client-side by `FlowRunner` and the flow label is not. The liveness README's translation cell says the same. These are comments only: a comment-stripped transpile of the three source files is byte-identical to before. +- `state-counts.md` is regenerated: `action` has 46 live and 0 planned (was 44 and 2); `translation` has 23 live and 1 planned (was 22 and 2). diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index ddcee1dcc7a..4570cdf9684 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -926,7 +926,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | job | seeded 2026-08-01 (#4488). The file-authored path is fully enforced: all three schedule shapes honored by the adapters, `retryPolicy`/`timeout` enforced since #3494 (this is the retryPolicy the datasource ledger warns about confusing with its dead namesake), `enabled: false` skips scheduling. Dead 3 = `id` (authorWarn — `name` is the identity everywhere) + label/description (docs-kept). The type-level gap CLOSED 2026-08-02 (#4509) by closing the door rather than bridging it: `handler` names a function in the compiled bundle's function table, which a runtime writer cannot name, so `allowRuntimeCreate` **and** `allowOrgOverride` are now false and `*.job.ts` / `defineStack({ jobs })` are the supported doors. The kind stays registered — its file loader is genuinely consumed (ADR-0088 admission test) **#4667**: `id` REMOVED (row deleted, strict removal) — nothing read it and its own describe() ("defaults to `name` when omitted") advertised an identity override that never existed; `name` is the scheduling key, the sys_job row key and the JobExecution.jobId stamp, so two jobs differing only in `id` were one job. **#7131** (PR #7425) takes the remaining two: `label` and `description` re-grade `dead` → `live` under the 2026-08-10 maintainer ruling that **designer previews count as consumers** — objectui's `JobPreview` had been reading `d.label` and `d.description` and rendering them as the preview card's title and subtitle the whole time, so the old "no runtime consumer" was a true statement about the *scheduler* and a false one about the system. **This row now has zero dead and the ADR-0033 exemption is still in force**, which is worth saying out loud because it is the first row in this table where those two facts hold together: the keys are still docs-shaped, still deliberately KEPT, still not `authorWarn`'d, and enforce-or-remove still has nothing to chase here. What changed is only that the exemption no longer has to carry the verdict — the measurement does. | | 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 | | 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` is the one that is not, and is `planned`. **#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. | +| 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. | | 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). Dead 5 = `name` (the file name is the suite identity; the CLI prints `path.basename`), `scenarios.name` (describe() says "for test reports"; every report carries `scenarioId` instead), `scenarios.description` (docs-shaped, kept), and the two on the enforce-or-remove worklist — `scenarios.tags` promises filtering that `os test`'s two flags cannot express, and `scenarios.requires` declares param/plugin preconditions nothing checks, so a suite naming a missing plugin runs anyway and fails as an unexplained HTTP error. Neither carries `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/action.json b/packages/spec/liveness/action.json index 82b592d8db0..756f2e7b0b5 100644 --- a/packages/spec/liveness/action.json +++ b/packages/spec/liveness/action.json @@ -199,16 +199,20 @@ "onSuccess": { "children": { "navigate": { - "status": "planned", - "verifiedAt": "2026-08-18", + "status": "live", + "verifiedAt": "2026-09-27", "evidenceScope": "cross-repo", - "note": "PLANNED, deliberately not `live` — the #9340 map / #9463 viewMode convention for a spec-first contract-split key. Declared by the #9566/#9474 maintainer ruling (2026-08-18, one navigation contract for both cards): a post-success route/URL template for type:'api'/'script' actions whose interpolation scope gains `${result.*}` (the server response) beside `${param.*}`/`${ctx.*}`. No console consumer reads it yet: objectui's consoleServerAction.ts drives only the handler-return `{ redirectUrl }` convention (new-tab, objectui#2967/#2904), executeAPI returns {success,data} and never navigates, and interpolateTarget's scope has no `result` member — the SPA-navigation branch, executeAPI navigation handling and result-scope interpolation are the downstream objectui card(s) filed Blocked-by #9566/#9474 at this key's landing. Amend to `live` citing the consoleServerAction/executeAPI read and the interpolateTarget result member when that half lands — measured objectui per the #9566 issue audit at spec/console 17.0.0, 2026-08-18." + "evidence": "objectui @f8a9d0fb: packages/core/src/actions/ActionRunner.ts#handlePostExecution (on a successful action, `readOnSuccessNavigation(action.onSuccess)` shape-guards the block and hands it to `navigateOnSuccess`); objectui @f8a9d0fb: packages/core/src/actions/ActionRunner.ts#navigateOnSuccess (interpolates `block.navigate` through `interpolateTarget` with the `${param.*}`, `${ctx.*}` and `${result.*}` scopes, the last one being the handler's own return value, refuses a URL that is neither http(s) nor relative, and hands the result to the host's `navigationHandler`); objectui @f8a9d0fb: packages/app-shell/src/hooks/useConsoleActionRuntime.tsx#useConsoleActionRuntime (the console's `navigateHandler`, wired as the runner's `onNavigate`: a relative URL is a react-router `navigate`, an external or new-tab one opens a window)", + "producer": "objectui @f8a9d0fb: packages/components/src/renderers/action/action-button.tsx#ActionButtonRenderer (forwards `onSuccess: schema.onSuccess` into the def the runner executes); objectui @f8a9d0fb: packages/components/src/renderers/action/action-icon.tsx#ActionIconRenderer (the same forward); objectui @f8a9d0fb: packages/components/src/renderers/action/action-menu.tsx#ActionMenuRenderer (the same forward); objectui @f8a9d0fb: packages/components/src/renderers/action/action-group.tsx#ActionGroupRenderer (the same forward); objectui @f8a9d0fb: packages/app-shell/src/views/DeclaredActionsBar.tsx#DeclaredActionButton (spreads the declared action, `onSuccess` included, into its dispatch)", + "note": "FLIPPED planned → live 2026-09-27 (#20296). It was PLANNED under the #9340 map / #9463 viewMode convention for a spec-first contract-split key: the #9566/#9474 maintainer ruling (2026-08-18) declared one post-success route/URL template for type:'api'/'script' actions whose interpolation scope gains `${result.*}` (the server response) beside `${param.*}`/`${ctx.*}`, and at spec/console 17.0.0 no console consumer read it (consoleServerAction drove only the handler-return `{ redirectUrl }` convention, and interpolateTarget had no `result` member). The objectui half has since landed (objectui#5221 for the runner hop, objectui#5493 for the renderer forward, and objectui#5934, which retired the runner's older chained-callback meaning of this key), and the `.objectui-sha` pin f8a9d0fb carries it, so every pointer above was read at the pin this repo builds against. Each read is unchanged at objectui main 256b4c9e. The hop sits in `handlePostExecution`, after the executor for every action type, so it is not an executeAPI branch: `api` actions reach it through the console's `api` handler and `script` actions through its server-action handler. That handler (objectui app-shell consoleServerAction.ts) defers to a declared block rather than also following a handler-returned `redirectUrl`, so one success makes one navigation; the schema refuses `onSuccess` beside `opensInNewTab: true`. `${result.*}` reads `readActionPayload(result.data)`, one level below the action envelope, and `navigateOnSuccess` is the only caller that passes a result scope. MOUNT CHAIN, closed by hand at the pin: the console root (apps/console App.tsx) mounts `ConsoleShell`, whose `useConsoleActionRuntime` feeds ``; @object-ui/react's ActionContext builds the `ActionRunner` and calls `setNavigationHandler(onNavigate)`. A declared action rendered by `action:button` / `action:icon` / `action:menu` / `action:group` (ObjectView's list toolbar renders `action:bar`) or by DeclaredActionsBar reaches `ActionRunner.execute` with `onSuccess` on the def (`producer`)." }, "openIn": { - "status": "planned", - "verifiedAt": "2026-08-18", + "status": "live", + "verifiedAt": "2026-09-27", "evidenceScope": "cross-repo", - "note": "PLANNED with its sibling `navigate` (see that entry for the full cross-repo measurement). The closed enum 'self'|'newTab' with a MATERIALIZED .default('self') — parse output always carries the resolved member, so the future console branch reads it with no fallback of its own. The shipped handler-return surface `{ redirectUrl, openIn? }` keeps 17.0.0 semantics (absent openIn ⇒ new-tab); only this schema key defaults 'self'. Amend to `live` together with `navigate` when the objectui half (Blocked-by #9566/#9474) lands." + "evidence": "objectui @f8a9d0fb: packages/core/src/actions/ActionRunner.ts#navigateOnSuccess (`const newTab = block.openIn === 'newTab'` reads the one member that changes the branch, with no fallback of its own, and passes `newTab` to the host's `navigationHandler`, or opens a window itself when no handler is registered); objectui @f8a9d0fb: packages/app-shell/src/hooks/useConsoleActionRuntime.tsx#useConsoleActionRuntime (the console's `navigateHandler` opens a new window on `newTab`, and otherwise makes a react-router `navigate`)", + "producer": "packages/spec/src/ui/action.zod.ts#ActionSchema (materializes `.default('self')` at parse, so parsed metadata always carries the resolved member); objectui @f8a9d0fb: packages/components/src/renderers/action/action-button.tsx#ActionButtonRenderer (forwards the whole `onSuccess` block, `openIn` included, into the def the runner executes; action-icon, action-menu, action-group and DeclaredActionsBar forward it the same way, see the `navigate` row)", + "note": "FLIPPED planned → live 2026-09-27 (#20296), together with its sibling `navigate`, whose note carries the history (planned under the #9340 / #9463 spec-first convention, Blocked-by #9566/#9474), the objectui cards that landed the reader, and the mount chain. Everything here was read at the `.objectui-sha` pin f8a9d0fb and is unchanged at objectui main 256b4c9e. The closed enum 'self'|'newTab' carries a MATERIALIZED .default('self'), and the reader relies on it as designed: it tests `=== 'newTab'` and writes no `?? 'self'`, so the default has one source of truth. A stored row rehydrated unparsed (#3903) that omits the key lands on the same branch, the in-place hop. The two `openIn` spellings never cross: the reader reads `onSuccess.openIn` only, never the top-level `type:'url'` switch spelled 'new-tab'. The shipped handler-return surface `{ redirectUrl, openIn? }` keeps its 17.0.0 semantics at the pin (absent openIn ⇒ new tab; objectui app-shell consoleServerAction.ts), and only this schema key defaults 'self'." } } }, diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index 585f3d78237..1400db3e142 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -30,7 +30,7 @@ for both corollaries. | `object` | 50 | 0 | 0 | 0 | 1 | 51 | | `field` | 91 | 0 | 0 | 1 | 1 | 93 | | `flow` | 34 | 0 | 0 | 6 | 0 | 40 | -| `action` | 44 | 0 | 0 | 3 | 2 | 49 | +| `action` | 46 | 0 | 0 | 3 | 0 | 49 | | `hook` | 19 | 0 | 0 | 3 | 0 | 22 | | `permission` | 36 | 0 | 0 | 6 | 0 | 42 | | `position` | 12 | 0 | 0 | 0 | 0 | 12 | @@ -52,7 +52,7 @@ for both corollaries. | `job` | 15 | 0 | 0 | 1 | 0 | 16 | | `mapping` | 14 | 0 | 0 | 0 | 0 | 14 | | `seed` | 13 | 0 | 0 | 0 | 0 | 13 | -| `translation` | 22 | 0 | 0 | 0 | 2 | 24 | +| `translation` | 23 | 0 | 0 | 0 | 1 | 24 | | `validation` | 18 | 0 | 0 | 0 | 0 | 18 | | `api` | 25 | 0 | 0 | 1 | 2 | 28 | | `capability` | 12 | 0 | 0 | 0 | 0 | 12 | @@ -67,4 +67,4 @@ for both corollaries. | `sharing_rule` | 16 | 0 | 0 | 0 | 1 | 17 | | `connector` | 29 | 0 | 0 | 44 | 1 | 74 | | `analytics_cube` | 17 | 0 | 0 | 10 | 0 | 27 | -| **total** | **933** | **5** | **1** | **168** | **12** | **1119** | +| **total** | **936** | **5** | **1** | **168** | **9** | **1119** | diff --git a/packages/spec/liveness/translation.json b/packages/spec/liveness/translation.json index 45722cbe747..462bffef36f 100644 --- a/packages/spec/liveness/translation.json +++ b/packages/spec/liveness/translation.json @@ -96,7 +96,7 @@ "status": "planned", "verifiedAt": "2026-08-11", "authorWarn": true, - "authorHint": "No shipped screen-flow runner reads this group yet — until the objectui half of #7646 lands, a `type: 'screen'` flow renders the strings authored on the flow (`config.title`, `fields[].label`, `fields[].placeholder`) in every locale.", + "authorHint": "Only part of this group is read. The console's screen-flow runner reads `screens`: each screen's `title` and each field's `label` / `placeholder` render in the active locale. The flow's own `label` is read by nothing yet, so a translated flow label is stored and never shown, and the flow keeps the label authored on it in every locale.", "children": { "label": { "status": "planned", @@ -104,12 +104,15 @@ "note": "Overlays `Flow.label`. Same `planned` verdict as its container and for the same reason — declared here, read by no shipped runner yet — but declared per key rather than inherited: the two halves resolve through DIFFERENT consumers when the runner lands (the launcher/wizard header reads the flow label, the screen renderer reads `screens`), so they can go `live` on different days and a blanket verdict would hide the first flip." }, "screens": { - "status": "planned", - "verifiedAt": "2026-08-11", - "note": "Per-screen heading + per-field copy, keyed by `FlowNode.id` / `ScreenFieldConfig.name` — the identifiers the client already holds as `ScreenSpec.nodeId` / `ScreenFieldSpec.name`. `planned` with its container: the screen-flow runner half is a downstream objectui card. Deeper conventions (`screens..title`, `screens..fields..{label,placeholder}`) are governed by that runner, not by ledger rows — the one-drill-level boundary this ledger's type note states." + "status": "live", + "verifiedAt": "2026-09-27", + "evidenceScope": "cross-repo", + "evidence": "packages/spec/src/system/i18n-resolver.ts#resolveFlowScreenTitle (reads `flows..screens..title` down the locale chain and falls back to the screen's own title); objectui @f8a9d0fb: packages/app-shell/src/views/FlowRunner.tsx#localizeScreen (the heading through `resolveFlowScreenTitle`, and each field's `label` / `placeholder` from `bundle[language]?.flows?.[flowName]?.screens?.[screen.nodeId]?.fields` over the spec's `FLOW_SCREEN_FIELD_COPY_KEYS`); objectui @f8a9d0fb: packages/app-shell/src/views/FlowRunner.tsx#activeFlowsBundle (reads the active language's `flows` group out of the i18next `translation` resource tree); objectui @f8a9d0fb: packages/app-shell/src/views/FlowRunner.tsx#FlowRunner (draws `shown.title` as the dialog title and hands the localized screen to `ScreenView`)", + "producer": "packages/runtime/src/app-plugin.ts#loadTranslations (hands each bundle's locale data to the i18n service whole, `flows` included); packages/runtime/src/domains/i18n.ts#handleI18nRequest (the translations route answers `getTranslations(locale)` with no group filter); objectui @f8a9d0fb: apps/console/src/loadLanguage.ts#loadLanguage (fetches the locale's translations and runs `transformSpecTranslations`); objectui @f8a9d0fb: packages/i18n/src/utils/spec-translations.ts#transformSpecTranslations (forwards every group it does not flatten, `flows` among them, verbatim under the `app` namespace); objectui @f8a9d0fb: packages/i18n/src/provider.tsx#I18nProvider (adds the loaded payload to the `translation` resource bundle that `activeFlowsBundle` reads)", + "note": "FLIPPED planned → live 2026-09-27 (#20296). It was PLANNED with its container under the #7646 contract-first split: the spec declared the vocabulary and the screen-flow runner half was a downstream objectui card. That half has landed client-side, the side #11287 picked (objectui#5920, FlowRunner's `localizeScreen`), and the `.objectui-sha` pin f8a9d0fb carries it, so every objectui pointer above was read at the pin this repo builds against. Each read is unchanged at objectui main 256b4c9e. Per-screen heading + per-field copy, keyed by `FlowNode.id` / `ScreenFieldConfig.name`, the identifiers the client already holds as `ScreenSpec.nodeId` / `ScreenFieldSpec.name`. Each key falls back to the authored string on its own. The screen `description` and the runner chrome stay untranslated here by ruling. Deeper conventions (`screens..title`, `screens..fields..{label,placeholder}`) are governed by the runner and the spec's `FLOW_SCREEN_COPY_KEYS` / `FLOW_SCREEN_FIELD_COPY_KEYS`, not by ledger rows: the one-drill-level boundary this ledger's type note states. MOUNT CHAIN, closed by hand at the pin: `FlowRunner` is mounted by `useConsoleActionRuntime`'s dialogs (the console root `ConsoleShell` renders them, and ObjectView / DeclaredActionsBar each run their own runtime), by `RecordDetailView`, and by the console's `developer/flow-runs` route (`FlowRunsPage`). A `type: 'flow'` action whose run pauses at a screen node opens it with `{ flowName, runId, screen }`. The bundle reaches it through the console root's `I18nProvider loadLanguage` (apps/console main.tsx). ⚠️ NOT flipped with it: the container `flows` keeps `planned` + `authorWarn` for its `label` child, which nothing reads yet (#20318), and its `authorHint` now says that `screens` is read and the flow label is not. That bit is group-level and has two readers: @objectstack/lint's warn map, and the CLI i18n coverage gate (`authorWarnedTranslationGroups` in packages/cli i18n-extract.ts), which holds back the whole `flows.*` demand while it is set. Dropping it before the label has a reader would switch on demand for `flows..label` too, so it drops when #20318 lands (seat ruling on #20296)." } }, - "note": "[#7646] Contract-first spec half of the screen-flow localization split, and `planned` is the honest status rather than `live` or `dead`: `dead` means declared with no consumer and no plan, while this group was ruled into the vocabulary by the maintainer specifically so the runner half could be built against it (the same ruling fixes the boundary — runner chrome, Cancel/Submit, stays in the console's own message catalog, NOT here). Addressing is measured against what the runner already holds: `flows..screens.` — the node id reaches the client verbatim as `ScreenSpec.nodeId` (packages/spec/src/contracts/automation-service.ts:138), which is also what correlates a resume back to its pause point — and `.fields.` (packages/spec/src/automation/builtin-node-config.zod.ts:382, forwarded as `ScreenFieldSpec.name`). Key face measured against `ScreenFieldConfigSchema`, not mirrored from the report: `label` + `placeholder` are declared, `help` is NOT — but ⚠️ no longer for its original reason: #17306 gave the screen field `ScreenFieldConfig.inlineHelpText`, so the help copy is REAL and what is missing is only THIS face's translation key for it. Growing that face is a ruled step against the #7646 enumeration, not a resolver-side accretion, so until it lands a `help` entry here would still parse clean and translate nothing — the ADR-0078 shape #6080 kept out of the page-component face, on a not-yet reason rather than an absent-key one; it rides `guidance` on the field surface instead, alongside `options`, which cannot be addressed by a value-keyed map because `ScreenFieldConfig.options[].value` is unconstrained. Flip to `live` with an objectui screen-flow-runner evidence pointer when the downstream consumer card lands; the resolver-side helper (a `FLOW_SCREEN_COPY_KEYS` sibling of `PAGE_COMPONENT_COPY_KEYS` in packages/spec/src/system/i18n-resolver.ts) is deliberately NOT in this change — #7634 was in flight on that file." + "note": "[#7646] Contract-first spec half of the screen-flow localization split, and `planned` is the honest status rather than `live` or `dead`: `dead` means declared with no consumer and no plan, while this group was ruled into the vocabulary by the maintainer specifically so the runner half could be built against it (the same ruling fixes the boundary — runner chrome, Cancel/Submit, stays in the console's own message catalog, NOT here). Addressing is measured against what the runner already holds: `flows..screens.` — the node id reaches the client verbatim as `ScreenSpec.nodeId` (packages/spec/src/contracts/automation-service.ts:138), which is also what correlates a resume back to its pause point — and `.fields.` (packages/spec/src/automation/builtin-node-config.zod.ts:382, forwarded as `ScreenFieldSpec.name`). Key face measured against `ScreenFieldConfigSchema`, not mirrored from the report: `label` + `placeholder` are declared, `help` is NOT — but ⚠️ no longer for its original reason: #17306 gave the screen field `ScreenFieldConfig.inlineHelpText`, so the help copy is REAL and what is missing is only THIS face's translation key for it. Growing that face is a ruled step against the #7646 enumeration, not a resolver-side accretion, so until it lands a `help` entry here would still parse clean and translate nothing — the ADR-0078 shape #6080 kept out of the page-component face, on a not-yet reason rather than an absent-key one; it rides `guidance` on the field surface instead, alongside `options`, which cannot be addressed by a value-keyed map because `ScreenFieldConfig.options[].value` is unconstrained. Flip to `live` with an objectui screen-flow-runner evidence pointer when the downstream consumer card lands; the resolver-side helper (a `FLOW_SCREEN_COPY_KEYS` sibling of `PAGE_COMPONENT_COPY_KEYS` in packages/spec/src/system/i18n-resolver.ts) is deliberately NOT in this change — #7634 was in flight on that file. 2026-09-27 (#20296): the `screens` child FLIPPED to `live` (objectui's FlowRunner reads it at the `.objectui-sha` pin f8a9d0fb; see that row), and `authorHint` was rewritten to say so. The container keeps `planned` + `authorWarn` for `label` alone, which nothing reads yet (#20318). The CLI i18n coverage gate keys its whole-group `flows.*` demand off this `authorWarn`, so dropping the bit waits for that reader, and then the whole group flips." }, "metadataForms": { "status": "live", diff --git a/packages/spec/src/system/i18n-resolver.ts b/packages/spec/src/system/i18n-resolver.ts index 755544466a0..9399cc27286 100644 --- a/packages/spec/src/system/i18n-resolver.ts +++ b/packages/spec/src/system/i18n-resolver.ts @@ -3515,10 +3515,14 @@ export function resolveFlowScreenTitle( * ⚠️ Deliberately NOT registered in {@link translateMetadataDocument}'s * dispatch table: that table reaches the REST metadata boundary by itself * (`TRANSLATABLE_METADATA_TYPES` drives `@objectstack/rest`, #3786), which - * would stand up a shipped reader of the `flows` group while its liveness - * rows are `planned` — the wiring decision (server-side vs client-side - * application) belongs to the downstream runner card of #11287, and the - * ledger flip rides that card, not this one. + * would make it a server-side reader of the `flows` group. The wiring + * decision (server-side vs client-side application) belonged to the + * downstream runner card of #11287, and that card took the client side. + * objectui's `FlowRunner` overlays `flows..screens` on the screen it + * draws, read at the `.objectui-sha` pin `f8a9d0fb`, and the ledger's + * `flows.screens` row is `live` citing it. This function stays unregistered + * because the server-side route is not the one taken. `flows..label` + * has no reader on either side yet, so its ledger row stays `planned`. */ export function translateFlow( flow: T, diff --git a/packages/spec/src/system/translation.zod.ts b/packages/spec/src/system/translation.zod.ts index 0234dfea2b0..5796e2cc0d4 100644 --- a/packages/spec/src/system/translation.zod.ts +++ b/packages/spec/src/system/translation.zod.ts @@ -1175,10 +1175,13 @@ const appTranslationDataShape = () => ({ * per-app bundle would ask every app to re-translate the platform (maintainer * ruling on #7646). * - * ⚠️ The runner half is a separate, downstream change: this declares the - * vocabulary and closes it, and no shipped runner reads it yet — see the - * `flows` row in `liveness/translation.json`, which is `planned` and carries - * that warning for authors. + * The runner half was a separate, downstream change, and it has landed + * client-side for the per-screen copy. objectui's `FlowRunner` reads + * `screens` (each screen's `title`, and each field's `label` and + * `placeholder`), measured at the `.objectui-sha` pin `f8a9d0fb`. The flow's own + * `label` is read by nothing yet. See the `flows` rows in + * `liveness/translation.json`: `screens` is `live`, `label` stays `planned`, + * and the group's author warning names the unread half. */ flows: z.record(z.string(), strictObject({ surface: 'this flow translation', diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index 9915b849a6a..e1c4193898f 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -1608,10 +1608,13 @@ const actionObject = () => strictObject({ * surface and defaults `'self'`; the handler convention is a shipped surface * and keeps new-tab. * - * The console consumer is not wired yet — the SPA navigation branch, - * `executeAPI` navigation handling and `${result.*}` interpolation are the - * downstream objectui half (Blocked-by #9566/#9474; tracked in the liveness - * ledger at `planned` strength with the amend-on-landing instruction). + * The console consumer is objectui's `ActionRunner` (`handlePostExecution` + * → `readOnSuccessNavigation` → `navigateOnSuccess`), read at the + * `.objectui-sha` pin `f8a9d0fb`. After an `api` or `script` action + * succeeds, it interpolates `navigate` with the `${param.*}`, `${ctx.*}` and + * `${result.*}` scopes and hands the URL to the console's router. It opens + * a new tab only on `openIn: 'newTab'`. The liveness ledger's two + * `onSuccess` rows are `live` and cite this reader. * * **The doubled channel is refused where the schema can see it** (#11519, * maintainer ruling 2026-08-24): a `type: 'script'` action declaring BOTH