diff --git a/.changeset/23710-liveness-print-flow-messages.md b/.changeset/23710-liveness-print-flow-messages.md new file mode 100644 index 00000000000..6340ecf4a70 --- /dev/null +++ b/.changeset/23710-liveness-print-flow-messages.md @@ -0,0 +1,17 @@ +--- +'@objectstack/spec': patch +--- + +fix(spec): a page that declares `print` is no longer warned that nothing applies it, and the flow terminal-toast translations read `live` (#23710) + +Clause-②: no + +**What was wrong.** The console now prints a page that declares `print`: its `PageRenderer` mounts a print stylesheet built from every key of the declaration (paper size and orientation, the four margins, the running header and footer, page numbers, and the two page-break hints). But the liveness ledger shipped with `@objectstack/spec` still graded `page.print` `planned` with an author warning, so `os validate` / `os build` / `os lint` told every author of a print page that printing it prints the screen layout. The `print` describes said the same ("until it ships, nothing applies it"). + +**What changes.** + +- `liveness/page.json`: `print`, its eight keys and the four `margins` sides read `live`, citing the console reader at the `.objectui-sha` pin. The container's `authorWarn` / `authorHint` are dropped, so `lintLivenessProperties` no longer emits `liveness-planned-property` for a page that declares `print`. +- `liveness/translation.json`: `flows.successMessage` and `flows.errorMessage` read `live`. The console's flow runner shows the translated terminal toast through `translateFlow` and falls back to the flow's own text when the bundle has none. +- `PagePrintSchema` and `PageSchema.print`: the describes name the console's browser print rendering as the consumer that applies each key. The server-side PDF renderer is still to come. + +Unchanged: what a `print` declaration accepts and refuses, and every other ledger row. diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 795849c5326..f43469d0580 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -213,7 +213,7 @@ View filter rule | **assignedProfiles** | `never` | optional | [REMOVED] `page.assignedProfiles` was removed in @objectstack/spec 17.5.0 (ADR-0090 D2, ADR-0049 enforce-or-remove) — it was named for the Profile concept ADR-0090 D2 deleted, and it gated nothing: no renderer, route or metadata read door ever read the key, so a page that "assigned profiles" stayed open to every caller who could reach it. Delete the key. Page audience is the permission set's: gate the DATA the page shows with the object's permission sets, and bind those sets to people through positions (named in a position's `permissionSets`) — those are the checks the runtime actually runs. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **interfaceConfig** | `{ source?: string; columns?: string[] \| object[]; sort?: object[]; filterBy?: object[]; … }` | optional | Interface-level page configuration (for Airtable-style interface pages) | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | -| **print** | `{ paperSize?: Enum<'A4' \| 'A5' \| 'Letter' \| 'Legal'>; orientation?: Enum<'portrait' \| 'landscape'>; margins?: object; repeatHeader?: boolean; … }` | optional | Print declaration — makes this page a print page, a document authored in the page's own blocks: paper size and orientation, margins in millimetres, the running header and footer (the page's own `header` / `footer` regions), page numbers and page-break hints, each mapped to print CSS. A print page is a `kind: 'full'` page of `type: 'record'`, `'home'` or `'app'` with its blocks in `regions`, and every block in it must come from the printable block subset: containers that draw every child (`page:section`, `page:card`, `page:footer`), field blocks (`record:details`, `record:highlights`), the child-record table `record:line_items`, `element:text`, `element:image`, `element:divider`, `element:definition-list`, `element:repeater`, `element:number` and `object-metric`; `os validate` / `os build` / `os lint` and the metadata save door refuse any other block inside it. Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, the declaration is validated but nothing applies it. A list view's print button is `allowPrinting`, not this. | +| **print** | `{ paperSize?: Enum<'A4' \| 'A5' \| 'Letter' \| 'Legal'>; orientation?: Enum<'portrait' \| 'landscape'>; margins?: object; repeatHeader?: boolean; … }` | optional | Print declaration — makes this page a print page, a document authored in the page's own blocks: paper size and orientation, margins in millimetres, the running header and footer (the page's own `header` / `footer` regions), page numbers and page-break hints, each mapped to print CSS. A print page is a `kind: 'full'` page of `type: 'record'`, `'home'` or `'app'` with its blocks in `regions`, and every block in it must come from the printable block subset: containers that draw every child (`page:section`, `page:card`, `page:footer`), field blocks (`record:details`, `record:highlights`), the child-record table `record:line_items`, `element:text`, `element:image`, `element:divider`, `element:definition-list`, `element:repeater`, `element:number` and `object-metric`; `os validate` / `os build` / `os lint` and the metadata save door refuse any other block inside it. Applied by the console's browser print rendering, which prints the page through the browser's own print dialog (where it can be saved as PDF); the server-side PDF renderer is to follow. A list view's print button is `allowPrinting`, not this. | | **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react'>` | optional (default: `"full"`) | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; ADR-0081 named it html), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors; react = real-React source executed at render by the runtime (ADR-0081), styled by inline `style` with the same token colors; it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via the OS_PAGE_REACT=off env toggle. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). | | **slots** | `{ header?: object \| object[]; actions?: object \| object[]; alerts?: object \| object[]; highlights?: object \| object[]; … }` | optional | Slot override map for slotted pages | | **source** | `string` | optional | Page source text. For kind==='html' it is constrained JSX compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors. For kind==='react' it is real React/JSX executed at render by @object-ui/react-runtime (trusted tier), styled by inline `style` with the same token colors. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). Authoritative over `regions` in both. | @@ -275,14 +275,14 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **paperSize** | `Enum<'A4' \| 'A5' \| 'Letter' \| 'Legal'>` | optional | Paper size — the size keyword of the print CSS `@page { size }` rule (A4 when omitted). Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it. | -| **orientation** | `Enum<'portrait' \| 'landscape'>` | optional | Sheet orientation — the orientation keyword of the `@page { size }` rule (portrait when omitted). Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it. | -| **margins** | `{ top?: number; right?: number; bottom?: number; left?: number }` | optional | Sheet margins in MILLIMETRES, one number per side — the `@page { margin }` rule; a side left out takes the renderer's default. Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it. | -| **repeatHeader** | `boolean` | optional | Repeat the page's `header` region (its `regions` entry named `header`) at the top of every printed sheet — the running header. The parse refuses it on a page with no `header` region. Off when omitted: the region prints once, at the top of the first sheet. Consumer: the console's browser print rendering; until it ships, nothing applies it. | -| **repeatFooter** | `boolean` | optional | Repeat the page's `footer` region (its `regions` entry named `footer`) at the bottom of every printed sheet — the running footer. The parse refuses it on a page with no `footer` region. Off when omitted: the region prints once, after the body. Consumer: the console's browser print rendering; until it ships, nothing applies it. | -| **pageNumbers** | `boolean` | optional | Print the sheet number and the sheet count in the bottom margin of every sheet — the `@page` margin box with `counter(page)` and `counter(pages)`. Off when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it. | -| **repeatTableHeaders** | `boolean` | optional | Page-break hint: repeat a table's column headings at the top of every sheet the table runs onto (`thead { display: table-header-group }`). On when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it. | -| **avoidBreakInside** | `boolean` | optional | Page-break hint: keep each block whole on one sheet when it fits, moving it to the next sheet instead of splitting it (`break-inside: avoid` on every block). Off when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it. | +| **paperSize** | `Enum<'A4' \| 'A5' \| 'Letter' \| 'Legal'>` | optional | Paper size — the size keyword of the print CSS `@page { size }` rule (A4 when omitted). Applied by the console's browser print rendering; the server-side PDF renderer is to follow. | +| **orientation** | `Enum<'portrait' \| 'landscape'>` | optional | Sheet orientation — the orientation keyword of the `@page { size }` rule (portrait when omitted). Applied by the console's browser print rendering; the server-side PDF renderer is to follow. | +| **margins** | `{ top?: number; right?: number; bottom?: number; left?: number }` | optional | Sheet margins in MILLIMETRES, one number per side — the `@page { margin }` rule; a side left out takes the renderer's default. Applied by the console's browser print rendering; the server-side PDF renderer is to follow. | +| **repeatHeader** | `boolean` | optional | Repeat the page's `header` region (its `regions` entry named `header`) at the top of every printed sheet — the running header. The parse refuses it on a page with no `header` region. Off when omitted: the region prints once, at the top of the first sheet. Applied by the console's browser print rendering. | +| **repeatFooter** | `boolean` | optional | Repeat the page's `footer` region (its `regions` entry named `footer`) at the bottom of every printed sheet — the running footer. The parse refuses it on a page with no `footer` region. Off when omitted: the region prints once, after the body. Applied by the console's browser print rendering. | +| **pageNumbers** | `boolean` | optional | Print the sheet number and the sheet count in the bottom margin of every sheet — the `@page` margin box with `counter(page)` and `counter(pages)`. Off when omitted. Applied by the console's browser print rendering. | +| **repeatTableHeaders** | `boolean` | optional | Page-break hint: repeat a table's column headings at the top of every sheet the table runs onto (`thead { display: table-header-group }`). On when omitted. Applied by the console's browser print rendering. | +| **avoidBreakInside** | `boolean` | optional | Page-break hint: keep each block whole on one sheet when it fits, moving it to the next sheet instead of splitting it (`break-inside: avoid` on every block). Off when omitted. Applied by the console's browser print rendering. | --- @@ -397,14 +397,14 @@ Print declaration: paper size, orientation, margins, the running header and foot | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **paperSize** | `Enum<'A4' \| 'A5' \| 'Letter' \| 'Legal'>` | optional | Paper size — the size keyword of the print CSS `@page { size }` rule (A4 when omitted). Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it. | -| **orientation** | `Enum<'portrait' \| 'landscape'>` | optional | Sheet orientation — the orientation keyword of the `@page { size }` rule (portrait when omitted). Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it. | -| **margins** | `{ top?: number; right?: number; bottom?: number; left?: number }` | optional | Sheet margins in MILLIMETRES, one number per side — the `@page { margin }` rule; a side left out takes the renderer's default. Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it. | -| **repeatHeader** | `boolean` | optional | Repeat the page's `header` region (its `regions` entry named `header`) at the top of every printed sheet — the running header. The parse refuses it on a page with no `header` region. Off when omitted: the region prints once, at the top of the first sheet. Consumer: the console's browser print rendering; until it ships, nothing applies it. | -| **repeatFooter** | `boolean` | optional | Repeat the page's `footer` region (its `regions` entry named `footer`) at the bottom of every printed sheet — the running footer. The parse refuses it on a page with no `footer` region. Off when omitted: the region prints once, after the body. Consumer: the console's browser print rendering; until it ships, nothing applies it. | -| **pageNumbers** | `boolean` | optional | Print the sheet number and the sheet count in the bottom margin of every sheet — the `@page` margin box with `counter(page)` and `counter(pages)`. Off when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it. | -| **repeatTableHeaders** | `boolean` | optional | Page-break hint: repeat a table's column headings at the top of every sheet the table runs onto (`thead { display: table-header-group }`). On when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it. | -| **avoidBreakInside** | `boolean` | optional | Page-break hint: keep each block whole on one sheet when it fits, moving it to the next sheet instead of splitting it (`break-inside: avoid` on every block). Off when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it. | +| **paperSize** | `Enum<'A4' \| 'A5' \| 'Letter' \| 'Legal'>` | optional | Paper size — the size keyword of the print CSS `@page { size }` rule (A4 when omitted). Applied by the console's browser print rendering; the server-side PDF renderer is to follow. | +| **orientation** | `Enum<'portrait' \| 'landscape'>` | optional | Sheet orientation — the orientation keyword of the `@page { size }` rule (portrait when omitted). Applied by the console's browser print rendering; the server-side PDF renderer is to follow. | +| **margins** | `{ top?: number; right?: number; bottom?: number; left?: number }` | optional | Sheet margins in MILLIMETRES, one number per side — the `@page { margin }` rule; a side left out takes the renderer's default. Applied by the console's browser print rendering; the server-side PDF renderer is to follow. | +| **repeatHeader** | `boolean` | optional | Repeat the page's `header` region (its `regions` entry named `header`) at the top of every printed sheet — the running header. The parse refuses it on a page with no `header` region. Off when omitted: the region prints once, at the top of the first sheet. Applied by the console's browser print rendering. | +| **repeatFooter** | `boolean` | optional | Repeat the page's `footer` region (its `regions` entry named `footer`) at the bottom of every printed sheet — the running footer. The parse refuses it on a page with no `footer` region. Off when omitted: the region prints once, after the body. Applied by the console's browser print rendering. | +| **pageNumbers** | `boolean` | optional | Print the sheet number and the sheet count in the bottom margin of every sheet — the `@page` margin box with `counter(page)` and `counter(pages)`. Off when omitted. Applied by the console's browser print rendering. | +| **repeatTableHeaders** | `boolean` | optional | Page-break hint: repeat a table's column headings at the top of every sheet the table runs onto (`thead { display: table-header-group }`). On when omitted. Applied by the console's browser print rendering. | +| **avoidBreakInside** | `boolean` | optional | Page-break hint: keep each block whole on one sheet when it fits, moving it to the next sheet instead of splitting it (`break-inside: avoid` on every block). Off when omitted. Applied by the console's browser print rendering. | ### Nested Shape: `PagePrint.margins` diff --git a/packages/lint/src/lint-liveness-properties.test.ts b/packages/lint/src/lint-liveness-properties.test.ts index bc1397f3823..7f21f66303a 100644 --- a/packages/lint/src/lint-liveness-properties.test.ts +++ b/packages/lint/src/lint-liveness-properties.test.ts @@ -1970,13 +1970,13 @@ describe('the hint a warned row shows an author (#16094, #21096)', () => { (r) => (r.entry.authorWarn === true || r.entry.status === 'experimental') && r.entry.status !== 'live', ); // Anti-vacuity: an experimental row without an authorHint and with a note - // exists, so the leak this pin forbids is a real one. An opted-in row exists - // too, but since #23268 retired `object.externalSharingModel` every shipped - // opt-in carries an authorHint (the CONTROL branch below); the no-hint - // opt-in class is held by the SYNTHETIC case after this one. + // exists, so the leak this pin forbids is a real one. No shipped row opts in + // with `authorWarn` any more: #23268 retired `object.externalSharingModel`, + // and #23710 flipped the last one, `page.print`, `live`. So the CONTROL + // branch below may find no shipped row, and both opt-in classes (with an + // authorHint and without) are held by the SYNTHETIC case after this one. const leakers = (pred: (r: ShippedRow) => boolean) => warned.filter((r) => pred(r) && r.entry.authorHint === undefined && typeof r.entry.note === 'string'); - expect(warned.filter((r) => r.entry.authorWarn === true).length).toBeGreaterThan(0); expect(leakers((r) => r.entry.status === 'experimental').length).toBeGreaterThan(0); expect(leakers((r) => typeof r.entry.note === 'string' && TRACKER_ID.test(r.entry.note as string)).length).toBeGreaterThan(0); for (const row of warned) { @@ -2070,17 +2070,42 @@ describe('#21127 — a stack authoring `mapping.connectorSource` lints clean, an expect(findings.filter((f) => f.rule === LIVENESS_LEDGER_UNREADABLE)).toEqual([]); }); - it('REAL LEDGER (the control): a `planned` row with `authorWarn` still warns, in the same call', () => { - // `page.print` is `planned` + `authorWarn` in tree — the legal shape: a - // consumer is being built, keep the key, it does nothing yet. (The control - // was `object.externalSharingModel` until #23268 retired it.) - const findings = lintLivenessProperties({ + it('PLANTED (the control): a `planned` row with `authorWarn` still warns, in the same call', () => { + // No shipped row carries `planned` + `authorWarn` any more: the last one, + // `page.print`, went `live` with the console's print rendering (#23710; + // `object.externalSharingModel` held this control before #23268 retired + // it). So the legal shape is planted, through the same ledger seam. + const findings = lintWithPlantedGroup({ objects: [FX_ACCOUNT], - pages: [{ name: 'invoice_print', print: {} }], mappings: [FX_PULL], + translations: [{ 'zh-CN': { [SYNTH_WARNED_GROUP]: synthWarnedGroup } }], }); const planned = findings.filter((f) => f.rule === 'liveness-planned-property'); - expect(planned.map((f) => f.message).join(' | ')).toContain('`print`'); + expect(planned.map((f) => f.message).join(' | ')).toContain(`\`${SYNTH_WARNED_GROUP}\``); expect(findings.filter((f) => f.message.includes('connectorSource'))).toEqual([]); }); }); + +// #23710: `page.print` was `planned` + `authorWarn`, so every compile of a page +// declaring `print` told its author that printing the page prints the screen +// layout. The console's `PageRenderer` applies the declaration through +// objectui's `buildPagePrintStylesheet` at the `.objectui-sha` pin, so the row +// is `live` and the warning is gone. +describe('#23710 — a page declaring `print` lints without a planned-property warning', () => { + it('REAL LEDGER: no finding names `print`, in a call that still warns', () => { + expect(authorWarnedProperties('page').has('print')).toBe(false); + const findings = lintLivenessProperties({ + objects: [{ name: 'widget', externalSharingModel: 'read' }], + pages: [{ + name: 'invoice_print', + print: { paperSize: 'A4', margins: { top: 15 }, pageNumbers: true, repeatTableHeaders: true }, + }], + }); + expect(findings.filter((f) => f.rule === 'liveness-planned-property')).toEqual([]); + expect(findings.filter((f) => f.message.includes('`print'))).toEqual([]); + // Anti-vacuity: the same call, the same ledger load, still raises the + // object's `dead` tombstone row, so the silence is a reading. + expect(findings.filter((f) => f.message.includes('externalSharingModel'))).toHaveLength(1); + expect(findings.filter((f) => f.rule === LIVENESS_LEDGER_UNREADABLE)).toEqual([]); + }); +}); diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 48ad64304b1..98e850c1a32 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -965,7 +965,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | tool | the inert authoring surface is now REMOVED, not merely marked: `category`/`permissions`/`active`/`builtIn` retired 2026-07-30 (#3896 close-out) after `requiresConfirmation` set the precedent (#3715, ADR-0033 §2). `permissions` promised an invocation gate nothing enforced and `active:false` withdrew nothing — false compliance, same shape as rls.enabled. The `.strict()` ToolSchema rejects each retired key with its prescription; the `tool-inert-authoring-keys-removed` conversion strips them from authored sources | | skill | `permissions` REMOVED 2026-07 (#3704); `triggerPhrases` REMOVED 2026-07-30 (#3896 close-out sweep — phrases were never matched; activation is `triggerConditions` + the agent's `skills[]` + /skill-name pinning) | | dataset | `measures.certified` (declared-but-unenforced governance flag) REMOVED in 16.0 (#2377) | -| page | live, plus the one dead tombstone below, plus the `print` declaration's eleven `planned` rows (#22158, 2026-10-08: the container, its eight keys and the four `margins` sides): card ① of the #8346 ruling (B′) declares the print page spec-first, so the declaration's REFUSALS are live (the parse's `checkPagePrintComposition`, lint's `validatePrintPageBlocks`) while what its keys configure has no reader until the console's print rendering, card ②, lands. The container row carries `authorWarn`, so an author who writes `print` is told the layout is not yet applied. Its earlier `planned` row, `requires`, flipped `live` 2026-10-02 (#20871): the save door refuses an html page whose written list disagrees with its source and stores the derived one, and boot hydration reports a stored page whose list names a plugin the deployment's SDUI manifest does not carry. Dead `assignedProfiles` REMOVED 2026-09-12 (ADR-0090 D2 + ADR-0049 — a per-page audience list named for the concept D2 deleted, with zero readers in either repo, so the page was open to everyone who could reach it). The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Its prior `live` verdict is the #12516 class twice over: the objectui bridge it cited never existed (lit control — two sibling objectui citations in the same file resolve), and the entry carried no `verifiedAt`, so nothing ever re-asked | +| page | live, plus the one dead tombstone below. The `print` declaration (#22158, 2026-10-08: the container, its eight keys and the four `margins` sides) was declared spec-first by card ① of the #8346 ruling (B′) — its REFUSALS live (the parse's `checkPagePrintComposition`, lint's `validatePrintPageBlocks`), its keys `planned` with `authorWarn` on the container — and flipped `live` 2026-10-16 (#23710) once card ②, the console's print rendering, reached the `.objectui-sha` pin: `PageRenderer` mounts objectui's `buildPagePrintStylesheet`, which reads every key. `authorWarn` went with the flip, so an author who writes `print` is no longer told the layout is not applied. Its earlier `planned` row, `requires`, flipped `live` 2026-10-02 (#20871): the save door refuses an html page whose written list disagrees with its source and stores the derived one, and boot hydration reports a stored page whose list names a plugin the deployment's SDUI manifest does not carry. Dead `assignedProfiles` REMOVED 2026-09-12 (ADR-0090 D2 + ADR-0049 — a per-page audience list named for the concept D2 deleted, with zero readers in either repo, so the page was open to everyone who could reach it). The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Its prior `live` verdict is the #12516 class twice over: the objectui bridge it cited never existed (lit control — two sibling objectui citations in the same file resolve), and the entry carried no `verifiedAt`, so nothing ever re-asked | | view | list/form drilled via `children` (#2998 Track B); list.{responsive,performance} + form.{defaultSort,aria} REMOVED 2026-07-30 (#3896 close-out sweep — list aria/data stay live); **form.data was that sweep's one CORRECTION** — the removal attempt broke the build (`defineForm` writes `data.provider='schema'` onto every metadata form, `metadata-protocol` serves it), so it stands `live` with re-verified evidence; form.{buttons,defaults} live (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification. **The dead set is six, not the four removals above**: #4534 (the last #4001 batch, batch 6e) declared three CONTAINER-level keys this row had never classified — `name` and `label`, both `dead`, and `object`, `live`. All three are properties of the `views: [...]` *container*, not of a view: `name` is dead because authoring it changes nothing — an authored value restates the key the container already registers under or contradicts it — and `label` is container display metadata with no reader. Neither is `authorWarn`'d and both are deliberately KEPT — the metadata door itself stamps the save name into every saved view body (`normalizeViewMetadata`) and its overlay paths key on that copy, so tombstoning `name` would reject the platform's own saves (re-measured 2026-10-02, #20301 stage 2: the earlier attribution to artifact-shipped containers and the metadata-validation sweep was the door's stamp misread; neither carries one). `object` is the container's object binding, and it was *stripped on every parse* until #4534 declared it. Separately, the level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) is noted on parents and is **not** in the counts — one drill level only **#9340**: `list.map` declared — the eighth visualization block (`ListMapConfigSchema`), keys mirroring objectui plugin-map's documented read set. FLIPPED `planned` → `live` 2026-08-24 (#11442): objectui#5908 landed `resolveListMapConfig`, which merges the view-level `map` block over the legacy `options.map` bag before `ListView.tsx`'s `case 'map'` forwards it into `ObjectMap`, with the same merged config also feeding the visualization-switcher's capability gate so a view binding coordinates only in the spec block is no longer filtered out of `allowedVisualizations` either (objectui#5042) **#22611** (2026-10-10): `list.requiredPermissions` is this type's only `planned` row (the `list.map` precedent above flipped `live`) — the audience gate (a list of capabilities, all required), declared spec-first under the ruling of record 6095014058, letter A, with `authorWarn` and an `authorHint` because no server reads it until the `/meta` read gate applies it (#22639, the carrier). The warning reaches a container's `list` only: the lint flattens one level of `children`, so a named `listViews` entry is not walked, and the key's describe carries the not-enforced clause for that door | | report | dataset-bound (ADR-0021); the aria/performance LEDGER entries were stale — the keys left the schema in the report-liveness close-out; deleted 2026-07-30 as hygiene. Audit-era `chart` DEAD superseded (framework#1890 / #3441) — live on non-joined reports only: a `joined` report's container `chart` is refused and `blocks[].chart` was removed (#20161, 2026-09-27; nothing drew either) | | dashboard | ADR-0021 dataset widgets (#3251; DashboardWidgetSchema `.strict()`); `aria`/`performance` (and widget `performance` + PerformanceConfigSchema) REMOVED 2026-07-30 (#3896 close-out sweep — no renderer applied any of them); audit-era `globalFilters`/`dateRange` DEAD superseded (framework#2501) **#4956**: `widgets` DRILLED — the row jumps 20 → 41 classified because all 22 widget-level keys enter the count at once. They had never been classified at all: the entry carried one blanket `live` plus a `note` asserting they were classified "in the DashboardWidgetSchema subtree", and no such subtree existed in any of the 28 ledger files. That gap, not any evidence, is what carried `widgets[].responsive` through the #3896 sweep that removed both its sibling `widgets[].performance` and its literal namesake `view.responsive` — `view` is drilled, so `list.responsive` got asked and went out. New dead 6 = `responsive` (retired #4876/#4995, tombstone keeps the row) + `colorVariant` + `actionUrl`/`actionType`/`actionIcon` + `aria`. The action trio is the sharpest: no renderer draws a per-widget action button at all (every `actionUrl` read in DashboardRenderer is scoped to `header.actions[]`), yet `validate-dashboard-action-refs.ts` enforces reference integrity on it and its docblock calls it "the per-widget button" — a lint guarding an affordance that does not exist. `requiresService` is the counter-example worth remembering: dead by every objectui measurement, and LIVE server-side (`filterDashboardForUser`, ADR-0057 D10) — judging a widget key from the renderer repo alone would have retired an enforced gate. `compareTo` is `live` on ONE path only (inline object-provider charts); on the ADR-0021 dataset path the string arms are dropped and `{ offset }` throws in the executor. **#6774** moves the row 33/8 → 34/7: `colorVariant` CORRECTED dead → live 2026-08-09, the enforce leg of #5010 ruling B landing from the renderer side (objectui#3359 / PR objectui#3799, absorbed by pin `09987b68`). Worth reading beside `requiresService` above, because it is the same lesson from the other end — that row warns against judging a widget key from the renderer repo alone, and this one is a `dead` verdict that was correct in this repo AND correct in the renderer repo on the day it was measured, and stopped being either when a cross-repo decision was implemented. A ledger row is a claim with a timestamp; `verifiedAt` is what makes the claim re-askable. It also empties the dashboard warn set, so the author-side lint now says nothing about any widget key — `dashboard` stays in the lint's TYPE_COLLECTIONS all the same (the `webhook`/`email_template` resolved state). **#17385** DRILLS `widgets.chartConfig` — 14 per-key verdicts where the row had carried one blanket `live`, re-measured against `.objectui-sha` pin `53ded82bf7a4`: 12 live (the nine chrome keys `chartConfigPresentation` lowers, plus `xAxis`/`yAxis`/`series`, whose PRESENTATION merges onto the derived bindings while `ChartAxis.field` and `ChartSeries.name` are dropped so membership stays with the dataset) and dead 2 — `type`, which parses and does nothing because the widget's own `type` owns the chart family, and `aria`, which has no reader on either face. Both are pinned as NEGATIVES in objectui, which is what makes them re-askable rather than merely asserted. ⚠️ The drill made SIX containers one level further down visible for the first time (`xAxis`/`yAxis`/`series`/`annotations`/`interaction`/`aria`, 39 child keys); they are RECORDED, not drilled — fanning this row's verdicts down over them would manufacture verdicts, and the evidence work is a separate measurement. Note the cell's previous last stated position (`34/7`) had already drifted one `dead` behind the generated artifact before this change; the counts columns are generated and are the authority **#22611** (2026-10-10): `requiredPermissions` is this type's only `planned` row — the audience gate (a list of capabilities, all required) shared word for word with the list view's, declared spec-first under the ruling of record 6095014058, letter A, with `authorWarn` and an `authorHint` because no server reads it until the `/meta` read gate applies it (#22639, the carrier) | @@ -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. | +| 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. | | 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/page.json b/packages/spec/liveness/page.json index 6524304654b..05fab326b9b 100644 --- a/packages/spec/liveness/page.json +++ b/packages/spec/liveness/page.json @@ -74,73 +74,97 @@ "note": "ARIA attributes applied by objectui renderers." }, "print": { - "status": "planned", - "verifiedAt": "2026-10-08", - "authorWarn": true, - "authorHint": "Keep it: the print declaration is validated, and the printable block subset is enforced, but no renderer applies the paper, margins, running header and footer or page numbers yet — printing the page today prints the screen layout.", - "note": "PLANNED (#22158, card ① of the ruling of record 6051470224 on #8346, letter B′: \"A document is a page with a print declaration; no new template type\"). The declaration lands spec-first with its REFUSALS live — PageSchema's checkPagePrintComposition (packages/spec/src/ui/page.zod.ts#checkPagePrintComposition) refuses `print` on a page that does not print its own authored blocks, and @objectstack/lint's validatePrintPageBlocks (packages/lint/src/validate-print-page-blocks.ts#validatePrintPageBlocks) refuses a block outside PRINTABLE_PAGE_COMPONENT_TYPES inside a print page at os validate / os build / os lint and the page save door — while what the keys CONFIGURE (paper, margins, running header and footer, page numbers, page-break hints) has no reader anywhere yet. `authorWarn` per enforce-or-mark (the externalSharingModel shape): an author is told the layout is not yet applied, at every compile, until the console reads it. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit.", + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (builds the `@media print` sheet from every key below, plus the two rules every print page needs: everything that is not the print page, an ancestor of it or inside it is hidden on paper, and the ancestors' viewport-height clipping is released); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object); framework packages/spec/src/ui/page.zod.ts#checkPagePrintComposition (the parse refuses `print` on a page that does not print its own authored blocks); packages/lint/src/validate-print-page-blocks.ts#validatePrintPageBlocks (refuses a block outside PRINTABLE_PAGE_COMPONENT_TYPES inside a print page at os validate / os build / os lint and the page save door)", + "note": "FLIPPED planned → live 2026-10-16 (#23710). Card ② of the ruling of record 6051470224 on #8346 (letter B′: \"A document is a page with a print declaration; no new template type\") has landed: the console's `PageRenderer` mounts, for a page whose `print` is an object, the print stylesheet `buildPagePrintStylesheet` builds from the declaration, every rule inside `@media print`, so the page on screen is drawn as before and the browser's print dialog (which also saves as PDF) prints the declared paper, margins, running header and footer, page numbers and page-break hints. All eight keys and the four `margins` sides are read; see each child. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both. It was `planned` (verified 2026-10-08) with `authorWarn` per enforce-or-mark: the declaration landed spec-first with its REFUSALS live (the parse's `checkPagePrintComposition`, lint's `validatePrintPageBlocks`) while what the keys configure had no reader. `authorWarn` and `authorHint` are dropped in this edit: a `live` row refuses them, and an author who writes `print` is no longer told the layout is not applied. Still to come, and not this row's: the server-side PDF renderer (card ③) and the record-page \"generate PDF\" action (card ④).", "children": { "paperSize": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to the size keyword of `@page { size }`. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (the size keyword of `@page { size }`, written by `pageRule` only for an enum member (A4 when omitted or off-vocabulary)); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object)", + "note": "FLIPPED planned → live 2026-10-16 (#23710). Applied by the console's browser print rendering: the size keyword of `@page { size }`, written by `pageRule` only for an enum member (A4 when omitted or off-vocabulary). It was `planned` (verified 2026-10-08) because the key was declared spec-first (#22158) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." }, "orientation": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to the orientation keyword of `@page { size }`. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (the orientation keyword of `@page { size }`, written by `pageRule` only for `portrait` / `landscape` (portrait when omitted)); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object)", + "note": "FLIPPED planned → live 2026-10-16 (#23710). Applied by the console's browser print rendering: the orientation keyword of `@page { size }`, written by `pageRule` only for `portrait` / `landscape` (portrait when omitted). It was `planned` (verified 2026-10-08) because the key was declared spec-first (#22158) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." }, "margins": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to `@page { margin }`, one side per key, in millimetres. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit.", + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (one `@page` `margin-` descriptor per declared side, in millimetres, written by `pageRule` only for a finite non-negative number; a side left out keeps the console's default sheet margin); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object)", + "note": "FLIPPED planned → live 2026-10-16 (#23710). Applied by the console's browser print rendering: one `@page` `margin-` descriptor per declared side, in millimetres, written by `pageRule` only for a finite non-negative number; a side left out keeps the console's default sheet margin. It was `planned` (verified 2026-10-08) because the key was declared spec-first (#22158) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both.", "children": { "top": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to the top value of `@page { margin }`, in millimetres. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (`pageRule` writes `margin-top: mm` when `margins.top` is a finite non-negative number); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object)", + "note": "FLIPPED planned → live 2026-10-16 (#23710): the top sheet margin, in millimetres, written into `@page`. It was `planned` (verified 2026-10-08) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." }, "right": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to the right value of `@page { margin }`, in millimetres. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (`pageRule` writes `margin-right: mm` when `margins.right` is a finite non-negative number); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object)", + "note": "FLIPPED planned → live 2026-10-16 (#23710): the right sheet margin, in millimetres, written into `@page`. It was `planned` (verified 2026-10-08) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." }, "bottom": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to the bottom value of `@page { margin }`, in millimetres. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (`pageRule` writes `margin-bottom: mm` when `margins.bottom` is a finite non-negative number); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object)", + "note": "FLIPPED planned → live 2026-10-16 (#23710): the bottom sheet margin, in millimetres, written into `@page`. It was `planned` (verified 2026-10-08) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." }, "left": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to the left value of `@page { margin }`, in millimetres. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (`pageRule` writes `margin-left: mm` when `margins.left` is a finite non-negative number); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object)", + "note": "FLIPPED planned → live 2026-10-16 (#23710): the left sheet margin, in millimetres, written into `@page`. It was `planned` (verified 2026-10-08) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." } } }, "repeatHeader": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to a running header — the page's `header` region repeated on every sheet. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (the page's own `header` region placed in the `thead` of the running-region table, which the sheet's `runningRegionRules` turn back into a table on paper so the browser repeats it on every sheet); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#RunningRegions (draws the running-region table, `display: contents` on screen, only when the flag is `true` and the region exists)", + "note": "FLIPPED planned → live 2026-10-16 (#23710). Applied by the console's browser print rendering: the page's own `header` region placed in the `thead` of the running-region table, which the sheet's `runningRegionRules` turn back into a table on paper so the browser repeats it on every sheet. It was `planned` (verified 2026-10-08) because the key was declared spec-first (#22158) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." }, "repeatFooter": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to a running footer — the page's `footer` region repeated on every sheet. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (the page's own `footer` region placed in the `tfoot` of the running-region table, which the sheet's `runningRegionRules` turn back into a table on paper so the browser repeats it on every sheet); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#RunningRegions (draws the running-region table, `display: contents` on screen, only when the flag is `true` and the region exists)", + "note": "FLIPPED planned → live 2026-10-16 (#23710). Applied by the console's browser print rendering: the page's own `footer` region placed in the `tfoot` of the running-region table, which the sheet's `runningRegionRules` turn back into a table on paper so the browser repeats it on every sheet. It was `planned` (verified 2026-10-08) because the key was declared spec-first (#22158) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." }, "pageNumbers": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to an `@page` margin box with `counter(page)` and `counter(pages)`. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (the `@page` bottom-right margin box with `counter(page)` / `counter(pages)`, written by `pageRule` when the key is `true`); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object)", + "note": "FLIPPED planned → live 2026-10-16 (#23710). Applied by the console's browser print rendering: the `@page` bottom-right margin box with `counter(page)` / `counter(pages)`, written by `pageRule` when the key is `true`. It was `planned` (verified 2026-10-08) because the key was declared spec-first (#22158) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." }, "repeatTableHeaders": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to `thead { display: table-header-group }`. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (`thead { display: table-header-group }` for every table on the page (on when omitted); `false` writes `table-row-group`, which does not repeat); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object)", + "note": "FLIPPED planned → live 2026-10-16 (#23710). Applied by the console's browser print rendering: `thead { display: table-header-group }` for every table on the page (on when omitted); `false` writes `table-row-group`, which does not repeat. It was `planned` (verified 2026-10-08) because the key was declared spec-first (#22158) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." }, "avoidBreakInside": { - "status": "planned", - "verifiedAt": "2026-10-08", - "note": "PLANNED — maps to `break-inside: avoid` on every block. No reader in either repo: the key is new (#22158), so objectui at the `.objectui-sha` pin `a58626c88` and at main `cef0eee` has no `print` read at all. Carrier: card ② of the #8346 ruling (objectui — the print CSS mapping, the printable blocks and the record page's print action), to be filed once this declaration is on main; the server-side renderer is card ③. Flip to `live` citing the console reader at the `.objectui-sha` pin that carries it, and drop `authorWarn` / `authorHint` in that same edit." + "status": "live", + "verifiedAt": "2026-10-16", + "evidenceScope": "cross-repo", + "evidence": "objectui @daeb77fc84dd: packages/components/src/renderers/layout/page-print.ts#buildPagePrintStylesheet (`break-inside: avoid` on every block of a region, written when the key is `true`); objectui @daeb77fc84dd: packages/components/src/renderers/layout/page.tsx#PageRenderer (mounts the sheet only for a page whose `print` is an object)", + "note": "FLIPPED planned → live 2026-10-16 (#23710). Applied by the console's browser print rendering: `break-inside: avoid` on every block of a region, written when the key is `true`. It was `planned` (verified 2026-10-08) because the key was declared spec-first (#22158) with no reader in either repo. The reader landed in objectui 2312bd9a6 (objectui#11958), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 2312bd9a6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both." } } }, diff --git a/packages/spec/liveness/state-counts/page.md b/packages/spec/liveness/state-counts/page.md index cd180a4fb4c..813de2d65fa 100644 --- a/packages/spec/liveness/state-counts/page.md +++ b/packages/spec/liveness/state-counts/page.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `page` | 23 | 0 | 0 | 1 | 11 | 35 | +| `page` | 34 | 0 | 0 | 1 | 0 | 35 | diff --git a/packages/spec/liveness/state-counts/translation.md b/packages/spec/liveness/state-counts/translation.md index af5fb95a23a..6fb4ed0141f 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` | 27 | 0 | 0 | 0 | 2 | 29 | +| `translation` | 29 | 0 | 0 | 0 | 0 | 29 | diff --git a/packages/spec/liveness/translation.json b/packages/spec/liveness/translation.json index f721caa244a..fd99d1c2dfe 100644 --- a/packages/spec/liveness/translation.json +++ b/packages/spec/liveness/translation.json @@ -127,20 +127,20 @@ "note": "Overlays `Flow.label`. FLIPPED planned → live 2026-10-05. The reader landed client-side in objectui 39a3e91fa, and the `.objectui-sha` pin 0abd4f9f8 carries it (`git merge-base --is-ancestor 39a3e91fad 0abd4f9f8769` answers yes), so every objectui pointer above was read at the pin this repo builds against. The name a user reads for the flow resolves as the active language's `flows..label`, then the authored label the engine serves as `AutomationResult.flowLabel` (landed in commit 5363e2dc64), then the API name. It is drawn in the runner's dialog header, above the step heading, and in the completion toast in place of the API name; an authored `successMessage` still wins the toast, and has carried its own key since 2026-10-09 (#22507; the `successMessage` row). The read goes through the spec's own `translateFlow`, from the same bundle the screen copy is read from, so the address is the spec's and there is no second loader. It was `planned` (verified 2026-08-11) and declared per key rather than inherited, because the two halves resolve through different consumers and could go `live` on different days. They did: `screens` on 2026-09-27, `label` here. `FlowRunner` reading `label` through `translateFlow` for its header and completion toast (falling back to `AutomationResult.flowLabel`, then the API name) and each field's copy over `FLOW_SCREEN_FIELD_COPY_KEYS` was measured at objectui @0abd4f9f8 (citations moved out of packages/spec/src/system/i18n-resolver.ts and translation.zod.ts, #23434)." }, "successMessage": { - "status": "planned", - "verifiedAt": "2026-10-09", + "status": "live", + "verifiedAt": "2026-10-16", "evidenceScope": "cross-repo", - "evidence": "packages/spec/src/system/i18n-resolver.ts#translateFlow (overlays `flows..successMessage` down the locale chain, only where the flow authors a `successMessage`); packages/spec/src/system/i18n-resolver.ts#FLOW_TERMINAL_MESSAGE_KEYS (the key list the CLI skeleton writer and `translation.test.ts` share); packages/lint/src/validate-translation-references.ts#validateTranslationReferences (refuses a translated `successMessage` over a flow that declares none)", + "evidence": "packages/spec/src/system/i18n-resolver.ts#translateFlow (overlays `flows..successMessage` down the locale chain, only where the flow authors a `successMessage`); packages/spec/src/system/i18n-resolver.ts#FLOW_TERMINAL_MESSAGE_KEYS (the key list the CLI skeleton writer and `translation.test.ts` share); packages/lint/src/validate-translation-references.ts#validateTranslationReferences (refuses a translated `successMessage` over a flow that declares none); objectui @daeb77fc84dd: packages/app-shell/src/utils/flowResponse.ts#translatedFlowMessage (asks the spec's `translateFlow` for `flows..successMessage` under the run's flow name and locale, and keeps the served string when the bundle has none); objectui @daeb77fc84dd: packages/app-shell/src/utils/flowResponse.ts#interpretFlowResponse (the terminal-success arm hands the served `successMessage` to `translatedFlowMessage`); objectui @daeb77fc84dd: packages/app-shell/src/views/FlowRunner.tsx#FlowRunner (hands `interpretFlowResponse` this run's flow name and bundle, so the resume and both launch hosts through `judgeFlowLaunch` answer the same words)", "producer": "packages/spec/src/automation/flow.zod.ts#FlowSchema (`successMessage`, the flow's own completion text); packages/services/service-automation/src/engine.ts#AutomationEngine (copies `flow.successMessage` verbatim onto the terminal result, declared as `AutomationResult.successMessage` in packages/spec/src/contracts/automation-service.ts)", - "note": "[#22507] PLANNED — the spec half of a contract-first split, the shape `screens` took under #7646: the face declares the key and `translateFlow` applies it, and the console reader is a downstream objectui change. The engine serves the flow's `successMessage` verbatim (a plain string, never interpolated), so the overlay is client-side, exactly as for the `label` the engine serves as `flowLabel`: objectui's `FlowRunner` (packages/app-shell/src/views/FlowRunner.tsx, the terminal-success toast that draws `outcome.successMessage`) hands the served string to `translateFlow` under the flow name. No reader in objectui today — at main 20c6d351a the runner draws the served string untranslated — so until it lands a translated `successMessage` is stored and not shown. No `authorWarn`: a child row's warn reaches no file-authored bundle (the group is a record keyed by flow name; see the CLI's `authorWarnedTranslationGroups` note), and the consumer is the downstream runner change this note names. Flip to `live` citing the objectui reader at the `.objectui-sha` pin that carries it." + "note": "FLIPPED planned → live 2026-10-16 (#23710). The console reads the translated `successMessage`: objectui's `translatedFlowMessage` asks the spec's `translateFlow` for `flows..successMessage` over the string the engine serves verbatim, and falls back to that string when the bundle carries none. The reader landed in objectui 42d8e8db6 (objectui#12075), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 42d8e8db6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both. It was `planned` (#22507, verified 2026-10-09) as the spec half of a contract-first split, the shape `screens` and `label` took: the face declared the key and `translateFlow` applied it while the console still drew the served string untranslated. No `authorWarn`, then or now: a child row's warn reaches no file-authored bundle (the group is a record keyed by flow name; see the CLI's `authorWarnedTranslationGroups` note)." }, "errorMessage": { - "status": "planned", - "verifiedAt": "2026-10-09", + "status": "live", + "verifiedAt": "2026-10-16", "evidenceScope": "cross-repo", - "evidence": "packages/spec/src/system/i18n-resolver.ts#translateFlow (overlays `flows..errorMessage` down the locale chain, only where the flow authors a `errorMessage`); packages/spec/src/system/i18n-resolver.ts#FLOW_TERMINAL_MESSAGE_KEYS (the key list the CLI skeleton writer and `translation.test.ts` share); packages/lint/src/validate-translation-references.ts#validateTranslationReferences (refuses a translated `errorMessage` over a flow that declares none)", + "evidence": "packages/spec/src/system/i18n-resolver.ts#translateFlow (overlays `flows..errorMessage` down the locale chain, only where the flow authors a `errorMessage`); packages/spec/src/system/i18n-resolver.ts#FLOW_TERMINAL_MESSAGE_KEYS (the key list the CLI skeleton writer and `translation.test.ts` share); packages/lint/src/validate-translation-references.ts#validateTranslationReferences (refuses a translated `errorMessage` over a flow that declares none); objectui @daeb77fc84dd: packages/app-shell/src/utils/flowResponse.ts#translatedFlowMessage (asks the spec's `translateFlow` for `flows..errorMessage` under the run's flow name and locale, and keeps the served string when the bundle has none); objectui @daeb77fc84dd: packages/app-shell/src/utils/flowResponse.ts#flowFailureMessage (prefers the served `errorMessage`, handed to `translatedFlowMessage`, over the raw `error`); objectui @daeb77fc84dd: packages/app-shell/src/views/FlowRunner.tsx#FlowRunner (hands `interpretFlowResponse` this run's flow name and bundle, so the resume and both launch hosts through `judgeFlowLaunch` answer the same words)", "producer": "packages/spec/src/automation/flow.zod.ts#FlowSchema (`errorMessage`, the flow's own failure text); packages/services/service-automation/src/engine.ts#AutomationEngine (copies `flow.errorMessage` verbatim onto the terminal result, declared as `AutomationResult.errorMessage` in packages/spec/src/contracts/automation-service.ts)", - "note": "[#22507] PLANNED — the spec half of a contract-first split, the shape `screens` took under #7646: the face declares the key and `translateFlow` applies it, and the console reader is a downstream objectui change. The engine serves the flow's `errorMessage` verbatim (a plain string, never interpolated), so the overlay is client-side, exactly as for the `label` the engine serves as `flowLabel`: objectui's `flowFailureMessage` (packages/app-shell/src/utils/flowResponse.ts, read by the runner's resume and by both launch hosts through `judgeFlowLaunch`) hands the served string to `translateFlow` under the flow name. No reader in objectui today — at main 20c6d351a the runner draws the served string untranslated — so until it lands a translated `errorMessage` is stored and not shown. No `authorWarn`: a child row's warn reaches no file-authored bundle (the group is a record keyed by flow name; see the CLI's `authorWarnedTranslationGroups` note), and the consumer is the downstream runner change this note names. Flip to `live` citing the objectui reader at the `.objectui-sha` pin that carries it." + "note": "FLIPPED planned → live 2026-10-16 (#23710). The console reads the translated `errorMessage`: objectui's `translatedFlowMessage` asks the spec's `translateFlow` for `flows..errorMessage` over the string the engine serves verbatim, and falls back to that string when the bundle carries none. The reader landed in objectui 42d8e8db6 (objectui#12075), and the `.objectui-sha` pin daeb77fc84dd carries it (`git merge-base --is-ancestor 42d8e8db6 daeb77fc84dd` answers yes), so every objectui pointer here was read at the pin this repo builds against. Re-read when the pin moved f96f771140f1 → daeb77fc84dd (2026-10-16): the reader files are byte-identical at both. It was `planned` (#22507, verified 2026-10-09) as the spec half of a contract-first split, the shape `screens` and `label` took: the face declared the key and `translateFlow` applied it while the console still drew the served string untranslated. No `authorWarn`, then or now: a child row's warn reaches no file-authored bundle (the group is a record keyed by flow name; see the CLI's `authorWarnedTranslationGroups` note)." }, "screens": { "status": "live", diff --git a/packages/spec/scripts/liveness/check-liveness.test.ts b/packages/spec/scripts/liveness/check-liveness.test.ts index 79507d33345..5acd7499ce4 100644 --- a/packages/spec/scripts/liveness/check-liveness.test.ts +++ b/packages/spec/scripts/liveness/check-liveness.test.ts @@ -1626,6 +1626,22 @@ function liveRows(shape: 'container' | 'child'): WarnCarrier[] { return out; } +/** Every `planned` row (top-level or drilled child) that does not already opt into `authorWarn`, sorted. */ +function plannedRows(): WarnCarrier[] { + const out: WarnCarrier[] = []; + for (const file of readdirSync(LEDGERS).filter((f) => f.endsWith('.json')).sort()) { + const type = file.slice(0, -'.json'.length); + const props: Record = JSON.parse(readFileSync(path.join(LEDGERS, file), 'utf8')).props ?? {}; + for (const [prop, row] of Object.entries(props)) { + if (row?.status === 'planned' && row?.authorWarn !== true) out.push({ type, path: prop }); + for (const [child, crow] of Object.entries(row?.children ?? {})) { + if (crow?.status === 'planned' && crow?.authorWarn !== true) out.push({ type, path: `${prop}.${child}` }); + } + } + } + return out; +} + describe('check:liveness — a `live` row may not opt into `authorWarn` (#21127)', () => { let tmp: string; @@ -1652,19 +1668,33 @@ describe('check:liveness — a `live` row may not opt into `authorWarn` (#21127) // THE CONTROL, and the census. Green on a verbatim copy, and the line names // the population it asked: a `planned` row with `authorWarn` is the legal - // shape (a consumer is being built — keep the key, it does nothing yet), so - // the shipped ledgers carry some and the gate leaves them alone. - it('is green on the shipped ledgers, which carry warned `planned` rows and no warned `live` one', () => { - const { status, output } = runGate(); - expect(status, output).toBe(0); - expect(output).toContain('no `live` row opts into an author warning'); - const line = output.split('\n').find((l) => l.startsWith('author warnings:')) ?? ''; - expect(line, 'the gate must publish the population it asked').not.toBe(''); - const reached = Number(/^author warnings: (\d+) /.exec(line)?.[1] ?? 0); - expect(reached, 'a walk reaching zero warned rows is a degraded walk, not a clean tree').toBeGreaterThan(0); - expect(line).toMatch(/\bplanned [1-9]\d*\b/); - expect(line).toContain('; 0 on a `live` row.'); - expect(line).not.toContain('FORBIDDEN'); + // shape (a consumer is being built — keep the key, it does nothing yet), and + // the gate leaves it alone. The shipped ledgers no longer carry one (#23710 + // flipped the last, `page.print`, `live`), so the walk's reach is proved on a + // copy with one shipped `planned` row marked: the census must count it, or + // the walk is degraded. + it('is green on the shipped ledgers, and on a warned `planned` row, which the census counts', () => { + const authorWarnLine = (output: string) => output.split('\n').find((l) => l.startsWith('author warnings:')) ?? ''; + const reachedOf = (line: string) => Number(/^author warnings: (\d+) /.exec(line)?.[1] ?? Number.NaN); + + const shipped = runGate(); + expect(shipped.status, shipped.output).toBe(0); + expect(shipped.output).toContain('no `live` row opts into an author warning'); + const shippedLine = authorWarnLine(shipped.output); + expect(shippedLine, 'the gate must publish the population it asked').not.toBe(''); + expect(shippedLine).toContain('; 0 on a `live` row.'); + expect(shippedLine).not.toContain('FORBIDDEN'); + + const [carrier] = plannedRows(); + expect(carrier, 'no `planned` row in the shipped ledgers to carry the sample').toBeDefined(); + const planted = runGate(sampleWarned('planned', carrier)); + expect(planted.status, planted.output).toBe(0); + const plantedLine = authorWarnLine(planted.output); + expect(reachedOf(plantedLine), 'a walk that does not count the marked row is a degraded walk, not a clean tree') + .toBe(reachedOf(shippedLine) + 1); + expect(plantedLine).toMatch(/\bplanned [1-9]\d*\b/); + expect(plantedLine).toContain('; 0 on a `live` row.'); + expect(plantedLine).not.toContain('FORBIDDEN'); }); // The regression's own shape: a `live` CONTAINER row, whose status the graded diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index ca9f4721312..a005527afe6 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -1278,15 +1278,15 @@ const PAGE_AUDIENCE_WRONG_LAYER = // page numbering, and page-break hints, each mapped to print CSS. A printable // block subset is defined beside it and enforced at the authoring doors. // -// What this card does NOT ship, and why the declaration is honest anyway: the -// browser print mapping, the printable rendering of each block and the record -// page's print action are card ② (objectui), the headless-Chromium render -// service and the archive are card ③, and the record-page "generate PDF" -// action is card ④. Until ② lands, nothing reads `print`. The key is therefore -// declared with its consumer named in every `.describe()` and carried in the -// liveness ledger as `planned` with `authorWarn` (`liveness/page.json`), so an -// author who writes it is told the layout is validated but not yet applied. -// The REFUSALS are live from this card: the parse refuses a print declaration +// What this card did NOT ship: the browser print mapping, the printable +// rendering of each block and the record page's print action are card ② +// (objectui), the headless-Chromium render service and the archive are card ③, +// and the record-page "generate PDF" action is card ④. Card ② has landed: the +// console's `PageRenderer` mounts the print stylesheet objectui's +// `buildPagePrintStylesheet` builds from every key below, so the liveness +// ledger carries `print` and its keys as `live` (`liveness/page.json`), citing +// that reader at the `.objectui-sha` pin. Cards ③ and ④ are still to come. +// The REFUSALS are live from card ①: the parse refuses a print declaration // on a page that does not print its own authored blocks // ({@link checkPagePrintComposition}), and `os validate` / `os build` / // `os lint` and the metadata save door refuse a block outside the subset @@ -1412,9 +1412,10 @@ export const PRINT_REFUSED_PAGE_COMPONENT_TYPES: ReadonlyMap = n * * Minimal by ruling: paper size and orientation, margins, the running header * and footer, page numbering and the page-break hints, and nothing else. Every - * key names its print-CSS mapping and its consumer. Until the console's print - * rendering lands (card ② of the ruling), nothing reads any of them — the - * liveness ledger carries each as `planned`. + * key names its print-CSS mapping and its consumer. The console's browser print + * rendering (card ② of the ruling) applies every one of them — the liveness + * ledger carries each as `live`; the server-side PDF renderer (card ③) is to + * follow. */ export const PagePrintSchema = lazySchema(() => strictObject({ surface: 'this print declaration', @@ -1434,9 +1435,9 @@ export const PagePrintSchema = lazySchema(() => strictObject({ }, }, { paperSize: z.enum(['A4', 'A5', 'Letter', 'Legal']).optional() - .describe("Paper size — the size keyword of the print CSS `@page { size }` rule (A4 when omitted). Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it."), + .describe("Paper size — the size keyword of the print CSS `@page { size }` rule (A4 when omitted). Applied by the console's browser print rendering; the server-side PDF renderer is to follow."), orientation: z.enum(['portrait', 'landscape']).optional() - .describe("Sheet orientation — the orientation keyword of the `@page { size }` rule (portrait when omitted). Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it."), + .describe("Sheet orientation — the orientation keyword of the `@page { size }` rule (portrait when omitted). Applied by the console's browser print rendering; the server-side PDF renderer is to follow."), margins: strictObject({ surface: 'these print margins', history: 'A side these margins do not declare would otherwise be dropped in silence, and the sheet would print with the default margin there.', @@ -1447,17 +1448,17 @@ export const PagePrintSchema = lazySchema(() => strictObject({ bottom: z.number().min(0).optional().describe('Bottom margin, in millimetres'), left: z.number().min(0).optional().describe('Left margin, in millimetres'), }).optional() - .describe("Sheet margins in MILLIMETRES, one number per side — the `@page { margin }` rule; a side left out takes the renderer's default. Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it."), + .describe("Sheet margins in MILLIMETRES, one number per side — the `@page { margin }` rule; a side left out takes the renderer's default. Applied by the console's browser print rendering; the server-side PDF renderer is to follow."), repeatHeader: z.boolean().optional() - .describe("Repeat the page's `header` region (its `regions` entry named `header`) at the top of every printed sheet — the running header. The parse refuses it on a page with no `header` region. Off when omitted: the region prints once, at the top of the first sheet. Consumer: the console's browser print rendering; until it ships, nothing applies it."), + .describe("Repeat the page's `header` region (its `regions` entry named `header`) at the top of every printed sheet — the running header. The parse refuses it on a page with no `header` region. Off when omitted: the region prints once, at the top of the first sheet. Applied by the console's browser print rendering."), repeatFooter: z.boolean().optional() - .describe("Repeat the page's `footer` region (its `regions` entry named `footer`) at the bottom of every printed sheet — the running footer. The parse refuses it on a page with no `footer` region. Off when omitted: the region prints once, after the body. Consumer: the console's browser print rendering; until it ships, nothing applies it."), + .describe("Repeat the page's `footer` region (its `regions` entry named `footer`) at the bottom of every printed sheet — the running footer. The parse refuses it on a page with no `footer` region. Off when omitted: the region prints once, after the body. Applied by the console's browser print rendering."), pageNumbers: z.boolean().optional() - .describe("Print the sheet number and the sheet count in the bottom margin of every sheet — the `@page` margin box with `counter(page)` and `counter(pages)`. Off when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it."), + .describe("Print the sheet number and the sheet count in the bottom margin of every sheet — the `@page` margin box with `counter(page)` and `counter(pages)`. Off when omitted. Applied by the console's browser print rendering."), repeatTableHeaders: z.boolean().optional() - .describe("Page-break hint: repeat a table's column headings at the top of every sheet the table runs onto (`thead { display: table-header-group }`). On when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it."), + .describe("Page-break hint: repeat a table's column headings at the top of every sheet the table runs onto (`thead { display: table-header-group }`). On when omitted. Applied by the console's browser print rendering."), avoidBreakInside: z.boolean().optional() - .describe("Page-break hint: keep each block whole on one sheet when it fits, moving it to the next sheet instead of splitting it (`break-inside: avoid` on every block). Off when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it."), + .describe("Page-break hint: keep each block whole on one sheet when it fits, moving it to the next sheet instead of splitting it (`break-inside: avoid` on every block). Off when omitted. Applied by the console's browser print rendering."), }).describe('Print declaration: paper size, orientation, margins, the running header and footer, page numbering and page-break hints. Its presence makes the page a print page, whose blocks must come from the printable block subset.')); /** The page kinds a print page may take: the one that prints its authored `regions` and nothing else. */ @@ -1900,11 +1901,11 @@ export const PageSchema = lazySchema(() => strictObject({ * authored in the page's own blocks. See {@link PagePrintSchema} for the * keys and their print-CSS mapping, {@link PRINTABLE_PAGE_COMPONENT_TYPES} * for the blocks a print page may hold, and {@link checkPagePrintComposition} - * for the pages that may carry it. `planned` in the liveness ledger until the - * console's print rendering (card ②) reads it. + * for the pages that may carry it. `live` in the liveness ledger: the + * console's print rendering (card ②) applies it. */ print: PagePrintSchema.optional() - .describe("Print declaration — makes this page a print page, a document authored in the page's own blocks: paper size and orientation, margins in millimetres, the running header and footer (the page's own `header` / `footer` regions), page numbers and page-break hints, each mapped to print CSS. A print page is a `kind: 'full'` page of `type: 'record'`, `'home'` or `'app'` with its blocks in `regions`, and every block in it must come from the printable block subset: containers that draw every child (`page:section`, `page:card`, `page:footer`), field blocks (`record:details`, `record:highlights`), the child-record table `record:line_items`, `element:text`, `element:image`, `element:divider`, `element:definition-list`, `element:repeater`, `element:number` and `object-metric`; `os validate` / `os build` / `os lint` and the metadata save door refuse any other block inside it. Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, the declaration is validated but nothing applies it. A list view's print button is `allowPrinting`, not this."), + .describe("Print declaration — makes this page a print page, a document authored in the page's own blocks: paper size and orientation, margins in millimetres, the running header and footer (the page's own `header` / `footer` regions), page numbers and page-break hints, each mapped to print CSS. A print page is a `kind: 'full'` page of `type: 'record'`, `'home'` or `'app'` with its blocks in `regions`, and every block in it must come from the printable block subset: containers that draw every child (`page:section`, `page:card`, `page:footer`), field blocks (`record:details`, `record:highlights`), the child-record table `record:line_items`, `element:text`, `element:image`, `element:divider`, `element:definition-list`, `element:repeater`, `element:number` and `object-metric`; `os validate` / `os build` / `os lint` and the metadata save door refuse any other block inside it. Applied by the console's browser print rendering, which prints the page through the browser's own print dialog (where it can be saved as PDF); the server-side PDF renderer is to follow. A list view's print button is `allowPrinting`, not this."), /** * Override semantics for record pages.