Skip to content

Commit cfa4d74

Browse files
fix(spec,lint): page requires is live — refused at save, reported at load (#21451)
Fixes #20871 Clause-②: no ## Summary This is the spec half of #20312 stage ③. The engine half landed first: - the save door (stages ① and ②) landed as #20852; - the load report and the draft-promotion re-stamp (stage ③ engine half, #20870) landed as #21121 (`250dec897`). This PR makes the spec say what those landings made true. No runtime code changes. - **`packages/spec/liveness/page.json`**: the `requires` row moves from `planned` to `live`. It carries `verifiedAt: 2026-10-02` and `evidenceScope: in-repo`. Its `evidence` names the save door, the promotion re-stamp and the load report, each as `file#symbol`. Its `producer` names the host that supplies the second input, the deployment's SDUI component manifest. The liveness README's producer table asks for one, because the reader compares the authored value against something a caller supplies. - **`packages/spec/src/ui/page.zod.ts`**: the `requires` describe used to say "(validated at save and load)", while the ledger said "declared, not enforced yet". The describe and its TSDoc now state what happens: - At save, on a server that has the deployment's SDUI component manifest, a `kind: 'html'` page's source is compiled (alias `'jsx'` too). A written list that disagrees with the source is refused (`422 INVALID_METADATA`, `page-requires-disagrees-with-source`). A draft keeps the list until its publish, which refuses it. The derived list is stored. - At load, a stored page whose list names a plugin no manifest component carries is reported, and it is still served. - A server with no manifest checks neither, and says so once at boot. - **`packages/lint/src/authoring-rules.ts`**: `validateJsxPages` no longer shares the `RUNTIME_HEAVY_SOURCE_PARSE` reason ("parses authored source through typescript/sucrase"). It gets its own reason, `RUNTIME_HTML_SOURCE_COMPILED_AT_SAVE`. That constant's TSDoc no longer lists jsx page bodies. `validateReactPages` keeps the old reason, which is true for it (Sucrase). - **ADR-0087 guide entry**: the `reason` of `18.ui-html-page-div-refused.ts` now names the runtime save door. `migrations/registry.ts` was regenerated with `gen:migration-registry`, never by hand. The existing entry is amended rather than a new D3 entry added. Step 18 is unreleased (`@objectstack/spec` is at 17.6.0), the entries README makes an entry file the unit of edit, and `ace770d5fc` amended this same entry's `reason` the same way. - **Docs**: the only "validated at save and load" sentence under `content/docs/**` was the `requires` row of `content/docs/references/ui/page.mdx`. That tree is AUTO-GEN, rendered from the describe, so it was regenerated rather than hand-edited. It now matches the describe, and `check:docs` holds the two equal, so this PR adds no separate grep pin. The hand-written `content/docs/ui/pages.mdx` has no `requires` row and no such sentence. - **Counts**: `liveness/state-counts/page.md` was regenerated. `page` goes from 22 live and 1 planned to 23 live and 0 planned (24 classified). - **Changeset**: `patch` for `@objectstack/spec` and `@objectstack/lint`, with `Clause-②: no`. No accept set moves. ## Declared deviation from the claimed file surface `packages/spec/liveness/README.md` also changed: the `page` row of the hand-written state table. Its Notes cell said "live + one planned", which this PR makes false. It now records the flip. `check:liveness` holds the row set and the counts, but never a Notes cell's text. ## Premise checks - **A1, positions at `ceb4a939b4`**, all confirmed: - `liveness/page.json:9` was `planned`, with the note "save/load enforcement of plugin presence is deferred (M3b)". - `page.zod.ts:903` was the `requires` line. - `authoring-rules.ts:450`-`:451` held the "typescript/sucrase" reason. `validateJsxPages` used it at `:1108` and `validateReactPages` at `:1122`. - The guide entry was `migrations/entries/semantic/18.ui-html-page-div-refused.ts`. - **A2, is the authored value read, or only overwritten?** It is read, and refused when it disagrees. The two #20312 blocks of `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts` (`-t 20312`) give 17 passed and 39 skipped. They include the case "refuses a hand-written `requires` that disagrees with the source, naming each namespace". That case pins `{ code: 'INVALID_METADATA', status: 422 }` for three shapes: - an unused namespace; - a namespace no manifest component carries; - a used namespace left unlisted. So authoring the key changes runtime behaviour, which is the README's definition of `live`. - **A3, what `validateJsxPages` parses with.** `packages/lint/src/validate-jsx-pages.ts` imports `parseJsx` and `compile` from `@objectstack/sdui-parser`, whose `package.json` declares no dependencies. `@objectstack/metadata-protocol`'s `runtime-authoring-gate.ts` imports the same `compile` statically, so the kernel already loads it. The rule stays off the runtime surface for a different reason: the save door runs the same compile itself (`findHtmlPageSourceGaps`), under the same `jsx-CODE` rule ids. The new reason says that. - **A4, the guide entry's new prose**, checked against `main`: - `os serve` (which `dev` and `start` spawn) resolves the manifest from beside the served config, then from the console's copy (`registerDeploymentSduiManifest`); - the save door compiles html source against it on every publish; - a draft is judged at its publish; - a host with no manifest prints one boot line and stores pages unjudged; - rows at rest are not recompiled at load. - **A5, the docs.** See Summary. Studio's round trip of a stale stamp answering `422` is exactly what the new sentence describes (a written list that disagrees is refused), so the docs do not name it. objectui#11357 is closed. ## The readers and the producer (A2) | moment | role | file#symbol | |:--|:--|:--| | save | judges the authored list | `packages/metadata-protocol/src/runtime-authoring-gate.ts#findHtmlPageSourceGaps` | | save | stores the derived list | `packages/metadata-protocol/src/runtime-authoring-gate.ts#stampHtmlPageRequires` | | draft promotion | re-stamps the promoted body | `packages/metadata-protocol/src/protocol.ts#promoteDraftForPublish` (`deriveActiveBody`) | | load | reports an absent plugin | `packages/metadata-protocol/src/protocol.ts#reportPageRequiresAbsentAtLoad`, called from `loadMetaFromDb`, judged by `runtime-authoring-gate.ts#findPageRequiresAbsentFromManifest` | | producer | supplies the manifest | `packages/cli/src/utils/sdui-manifest.ts#registerDeploymentSduiManifest`, called from `packages/cli/src/commands/serve.ts` and read per publish and at load through `protocol.ts#resolveSduiManifest` | **The ledger gate reads the row.** As a one-shot ablation through `scripts/ablation-replace.mjs`, the evidence path `runtime-authoring-gate.ts#findHtmlPageSourceGaps` was rewritten to a file that does not exist. - `check:liveness` went red: "1 'live' / 'planned' / 'experimental' / 'live-elsewhere' entr(ies) cite a file that is missing from THIS repo: page/requires". - The same run reports "854 pointer(s) written `path#symbol`, 854 naming a symbol the cited file contains", so the cited symbols are held as well as the paths. - The restore was verified: blob `a866b58134` equals HEAD, and `git diff HEAD` is empty. ## Verification at `3e1f0dabff` This run resumed one that was lost to a container restart. Nothing from before the restart is cited. `origin/main` was merged through `scripts/pm/os-regen-merge.sh` (merge `3e1f0dabff`). `registry.ts` is not driver-routed, and both sides survived the text merge: this branch's step 18 text, and main's new `dashboard-widget-single-series-multi-measure-refused` entry. Every reading below is at `3e1f0dabff`. - **Build.** `turbo run build --filter='./packages/**'`: 71 of 71 tasks successful. The tree was clean afterwards. - **`@objectstack/spec`**: - `build`: exit 0. - `check:generated`: exit 0, "All 15 generated artifacts are up to date". - `check:liveness`: exit 0, "packages/spec/liveness/state-counts/ is current". - `test` (`vitest run --project local`, two shards): 300 files, 9053 passed and 1 todo; then 300 files, 8631 passed. Both exit 0. - `typecheck`: exit 0. - **`@objectstack/lint`**: `test` gives 119 files and 5585 passed, exit 0. `typecheck` exits 0. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) derived 110 commands. All 110 ran, each exit code written to disk before any reading, and all exited 0. `--ran` reconciles them: "110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN". - On the first pass, two were infrastructure non-measurements, not reds, and both were re-run green. - `check-adr-0087-registration --self-test` could not write its fixture commits: the container's commit-signing server answered `503`. On re-run: "441 assertions". - `check:query-options-erasure` hit the per-command 300s cap on a contended box. On re-run it exited 0 in 491s: "ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new". - **Named gates**, with their own verdict lines: - `pnpm check:adr-0087-registration`: "this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)". - `pnpm check:empty-changeset`: "No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)". - `check-changeset-no-major --base origin/main`: "This diff introduces no `major` bump". Driven offline against this body (`--event`): "LEVEL AXIS: this PR declares clause-② `no`, so no package here is declared to have grown a published surface". - `check-changeset-fixed`: the `.changeset/config.json` "fixed" group "is in sync with 69 public workspace packages". - `pnpm check:doc-authoring`: "17283 customer-facing string(s) across 1234 spec sources clean". - `pnpm check:nul-bytes`: "OK (scanned 9771 text file(s) ... no raw ASCII control bytes)". - Roster gates with a roster under these paths are all exit 0: `check:meta-url-spelling`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`. - **Lint, narrowed and declared.** `pnpm lint` is run by CI. Here: - Population: `eslint.config.mjs` lints `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`. Of the 9 changed files, exactly the 4 `.ts` files are in it. - Count: `eslint --no-inline-config --format json` over those 4 files gives 4 results, 0 errors and 0 warnings. - Invariance: the config never enables type-aware linting (no `parserOptions.project`, no `projectService`), so this diff cannot move the verdict on any untouched file. - **Mergeability.** `main` moved after the merge. A local `git merge-tree --write-tree HEAD origin/main` at `53fd35e3e3` is clean. None of this diff's driver-routed paths changed on `main`, so GitHub sees the same answer. CI judges the merge ref. ## Acceptance notes - `packages/lint/src/runtime-lazy-deps.test.ts`'s header says "The two rules that need them stay CLI-only (`RUNTIME_HEAVY_SOURCE_PARSE`)". After this PR, one registry rule (`validateReactPages`) carries that constant. This is test prose, not a published surface, and it is not edited here. Carrier: none. - The no-manifest boot line in `packages/cli/src/utils/sdui-manifest.ts` says "Page source and `requires` not validated at save". That host skips the load report too, so the line could say "at save or load". It is not false, it is in a `domain:cli` file pinned by the CLI's tests, and it stays out of scope here. Carrier: none. - A host with no manifest has its save door judge nothing, while `validateJsxPages` still checks syntax and structure without a manifest. The new reason's TSDoc records this. The host announces it at boot, so it is not a finding. - `skills/**`: zero hits for a page `requires` sentence or "validated at save and load". Nothing to list. - Review fix round: the reconciliation-ledger root `omit` row for `page` / `requires` (`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`) said "declared, not enforced yet", which this PR makes false; it is re-ledgered under "platform-written, never authored" on the schema's own words with the measured truth per page kind, and no form offer, per seat answer 5959584348 (commit `54c73b11ff`). --- _Generated by [Claude Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 99589f9 commit cfa4d74

10 files changed

Lines changed: 85 additions & 18 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/lint': patch
4+
---
5+
6+
`page.requires` says what the runtime now does with it: refused at save, reported at load (ADR-0080 §5).
7+
8+
Clause-②: no
9+
10+
The key's description used to say the list is "validated at save and load" while the liveness ledger recorded it as not enforced yet. Both are now true and say so. On a server that has the deployment's SDUI component manifest, saving a `kind: 'html'` page compiles its source, refuses a written `requires` that disagrees with it (`422 INVALID_METADATA`, `page-requires-disagrees-with-source`; a draft at its publish) and stores the derived list. At load, a stored page whose list names a plugin no manifest component carries is reported and still served. A server with no manifest checks neither and says so once at boot. Omit `requires`: it is derived from the source. The liveness row moves from `planned` to `live`, and the generated page reference carries the new description.
11+
12+
`validateJsxPages`' reason for staying off the runtime publish gate no longer says it parses through `typescript`/`sucrase`. It parses with the dependency-free `@objectstack/sdui-parser`, and it stays CLI-only because the save door already runs that compiler on every html page. The `ui-html-page-div-refused` upgrade-guide entry now names that save door too: on a server with a manifest, a `div` page saved from Studio or through the metadata API is refused under the same rule ids.
13+
14+
No schema accepts or refuses anything it did not before, and no runtime behaviour changes.

‎content/docs/references/ui/page.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ View filter rule
184184
| **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react' \| 'jsx'>` | optional (default: `"full"`) | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; the legacy value 'jsx' is a deprecated alias), 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 amendment 2026-06-30). |
185185
| **slots** | `{ header?: object \| object[]; actions?: object \| object[]; alerts?: object \| object[]; highlights?: object \| object[]; … }` | optional | Slot override map for slotted pages |
186186
| **source** | `string` | optional | Page source text. For kind==='html' (alias 'jsx') 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 amendment 2026-06-30). Authoritative over `regions` in both. |
187-
| **requires** | `string[]` | optional | Plugin namespaces the JSX source references (validated at save and load) |
187+
| **requires** | `string[]` | optional | Plugin namespaces the page's source uses, derived from the source at save — omit it. On a server that has the deployment's SDUI component manifest, saving a kind==='html' page (alias 'jsx') compiles its source and stores the namespaces it uses here; a written list that disagrees with the source is refused (422 INVALID_METADATA, page-requires-disagrees-with-source) — on a draft save it is kept until the draft's publish, which refuses it. At load, a stored page whose list names a plugin no component in that manifest carries is reported, page and plugin named, and is still served. A server with no manifest checks neither and says so once at boot. |
188188
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
189189
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
190190
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,16 +441,40 @@ const RUNTIME_NEEDS_FULL_SNAPSHOT =
441441
'now would report the rest of the tenant\'s metadata as missing rather than judging this write.';
442442

443443
/**
444-
* The rule parses authored SOURCE (react/jsx page bodies, L2 JS hook/action
444+
* The rule parses authored SOURCE (react page bodies, L2 JS hook/action
445445
* bodies) through `typescript` / `sucrase`. Those are exactly the dependencies
446446
* `lazy-deps.test.ts` keeps off the kernel boot path, and `@objectstack/lint`'s
447447
* runtime entry is guarded to load neither. Studio's page editor has its own
448448
* save-time compile path; this gate is not where that check belongs.
449+
*
450+
* The html tier's rule (`validateJsxPages`) is NOT this case — see
451+
* {@link RUNTIME_HTML_SOURCE_COMPILED_AT_SAVE}.
449452
*/
450453
const RUNTIME_HEAVY_SOURCE_PARSE =
451454
'Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the ' +
452455
'kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.';
453456

457+
/**
458+
* `validateJsxPages` parses an html page's source with `@objectstack/sdui-parser`
459+
* — no dependencies, never executes the source — so nothing about it is unsafe
460+
* on the kernel boot path. It stays off this registry's runtime surface because
461+
* the save door already runs the same compile itself: `findHtmlPageSourceGaps`
462+
* in `@objectstack/metadata-protocol`'s `runtime-authoring-gate.ts` imports the
463+
* same `compile()` and runs it against the deployment's SDUI component manifest,
464+
* reports under the same `jsx-CODE` rule ids, and adds the page's `requires`
465+
* check (`page-requires-disagrees-with-source`). Wiring this entry there too
466+
* would judge every html page twice.
467+
*
468+
* The two differ in one case: with no manifest this rule still checks syntax
469+
* and structure, while a host that registered no manifest has its save door
470+
* judge nothing and says so once at boot.
471+
*/
472+
const RUNTIME_HTML_SOURCE_COMPILED_AT_SAVE =
473+
'Runtime-safe (the dependency-free @objectstack/sdui-parser, which never executes the source) but ' +
474+
'not wired here: the save door already compiles an html page\'s source itself, with the same ' +
475+
'compiler against the deployment\'s SDUI component manifest and under the same jsx-* rule ids ' +
476+
'(metadata-protocol\'s findHtmlPageSourceGaps), so a second run would judge each page twice.';
477+
454478
/**
455479
* The rule judges an OBJECT/field declaration at `advisory` tier — it can
456480
* never refuse a write (`tier: 'advisory'` means it never emits `error`, and
@@ -1111,7 +1135,7 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
11111135
commands: ALL,
11121136
source: 'packages/lint/src/validate-jsx-pages.ts',
11131137
surfaces: CLI_ONLY,
1114-
surfaceReason: RUNTIME_HEAVY_SOURCE_PARSE,
1138+
surfaceReason: RUNTIME_HTML_SOURCE_COMPILED_AT_SAVE,
11151139
run: (stack, ctx) =>
11161140
validateJsxPages(stack, ctx.sduiManifest ? { manifest: ctx.sduiManifest as never } : {}),
11171141
},

‎packages/spec/liveness/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,7 @@ marker where the Notes cell goes, never a guess at what belongs there.
966966
| 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 |
967967
| 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) |
968968
| dataset | `measures.certified` (declared-but-unenforced governance flag) REMOVED in 16.0 (#2377) |
969-
| page | live + one planned; 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 |
969+
| page | live, plus the one dead tombstone below. Its one `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 |
970970
| 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) |
971971
| 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) |
972972
| 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 |

‎packages/spec/liveness/page.json‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
"note": "JSX-source page authoring (ADR-0080). Consumer: objectui PageRenderer compiles `source` via @object-ui/sdui-parser into the SchemaNode tree (parse, never execute) and renders it — components/src/renderers/layout/page.tsx (kind:'jsx' branch). Browser-verified in the Command Center showcase."
88
},
99
"requires": {
10-
"status": "planned",
11-
"note": "Plugin namespaces the JSX `source` references (ADR-0080). Inferred at compile time; save/load enforcement of plugin presence is deferred (M3b) — declared, not enforced yet."
10+
"status": "live",
11+
"verifiedAt": "2026-10-02",
12+
"evidenceScope": "in-repo",
13+
"evidence": "SAVE: packages/metadata-protocol/src/runtime-authoring-gate.ts#findHtmlPageSourceGaps reads the authored list of a kind 'html' page and refuses one that disagrees with the namespaces its compiled source uses — 422 INVALID_METADATA under page-requires-disagrees-with-source, on an active save and on a draft's publish; packages/metadata-protocol/src/runtime-authoring-gate.ts#stampHtmlPageRequires stores the compiled list on save, and packages/metadata-protocol/src/protocol.ts#promoteDraftForPublish applies it again to the body a draft promotion writes. LOAD: packages/metadata-protocol/src/protocol.ts#reportPageRequiresAbsentAtLoad, called from boot hydration (loadMetaFromDb), reports a stored page whose list names a namespace no component in the manifest carries (packages/metadata-protocol/src/runtime-authoring-gate.ts#findPageRequiresAbsentFromManifest), page and plugin named; the page still loads and is served",
14+
"producer": "packages/cli/src/utils/sdui-manifest.ts#registerDeploymentSduiManifest — both moments compare the list against the deployment's SDUI component manifest, a second input: os serve (packages/cli/src/commands/serve.ts, which dev and start spawn) resolves it at boot and registers it under SDUI_MANIFEST_SERVICE, and packages/metadata-protocol/src/protocol.ts#resolveSduiManifest reads that key per publish and at load. A host that registers no manifest judges neither moment and prints one boot line saying so",
15+
"note": "Plugin namespaces an html page's `source` uses (ADR-0080 §5), derived from the source at save. planned → live 2026-10-02 (#20871): refused at save since the save door landed (PR #20852, #20312 stages ① and ②), reported at load and re-stamped on draft promotion since #20870 (PR #21121). An authored list survives only when it agrees with the source; omitting it is the intended authoring. Boundaries: kind 'react' pages are not compiled at save (ADR-0081), so an authored list on one is judged only by the load report; a draft is stored as written and judged at its publish; rows already stored are reported, never rewritten."
1216
},
1317
"name": {
1418
"status": "live",

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

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

1313
| Type | live | exp | elsewhere | dead | planned | classified |
1414
|---|---|---|---|---|---|---|
15-
| `page` | 22 | 0 | 0 | 1 | 1 | 24 |
15+
| `page` | 23 | 0 | 0 | 1 | 0 | 24 |

0 commit comments

Comments
 (0)