From 0e521b327e255b43b580b3d38293c2b74988cef0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 14:27:06 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(spec,lint):=20page=20requires=20is=20l?= =?UTF-8?q?ive=20=E2=80=94=20refused=20at=20save,=20reported=20at=20load?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The liveness row flips planned to live, citing the save door and the load report; the describe states what happens at save and load; the validateJsxPages surface reason no longer names typescript/sucrase; the ui-html-page-div-refused guide entry names the runtime save door. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- .changeset/20871-page-requires-live.md | 14 ++++++++++ packages/lint/src/authoring-rules.ts | 28 +++++++++++++++++-- packages/spec/liveness/README.md | 2 +- packages/spec/liveness/page.json | 8 ++++-- .../semantic/18.ui-html-page-div-refused.ts | 10 ++++++- packages/spec/src/ui/page.zod.ts | 13 +++++++-- 6 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 .changeset/20871-page-requires-live.md diff --git a/.changeset/20871-page-requires-live.md b/.changeset/20871-page-requires-live.md new file mode 100644 index 00000000000..b9cc60b784c --- /dev/null +++ b/.changeset/20871-page-requires-live.md @@ -0,0 +1,14 @@ +--- +'@objectstack/spec': patch +'@objectstack/lint': patch +--- + +`page.requires` says what the runtime now does with it: refused at save, reported at load (ADR-0080 §5). + +Clause-②: no + +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. + +`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. + +No schema accepts or refuses anything it did not before, and no runtime behaviour changes. diff --git a/packages/lint/src/authoring-rules.ts b/packages/lint/src/authoring-rules.ts index 621d0243601..2dd8fe68b0f 100644 --- a/packages/lint/src/authoring-rules.ts +++ b/packages/lint/src/authoring-rules.ts @@ -441,16 +441,40 @@ const RUNTIME_NEEDS_FULL_SNAPSHOT = 'now would report the rest of the tenant\'s metadata as missing rather than judging this write.'; /** - * The rule parses authored SOURCE (react/jsx page bodies, L2 JS hook/action + * The rule parses authored SOURCE (react page bodies, L2 JS hook/action * bodies) through `typescript` / `sucrase`. Those are exactly the dependencies * `lazy-deps.test.ts` keeps off the kernel boot path, and `@objectstack/lint`'s * runtime entry is guarded to load neither. Studio's page editor has its own * save-time compile path; this gate is not where that check belongs. + * + * The html tier's rule (`validateJsxPages`) is NOT this case — see + * {@link RUNTIME_HTML_SOURCE_COMPILED_AT_SAVE}. */ const RUNTIME_HEAVY_SOURCE_PARSE = 'Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the ' + 'kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.'; +/** + * `validateJsxPages` parses an html page's source with `@objectstack/sdui-parser` + * — no dependencies, never executes the source — so nothing about it is unsafe + * on the kernel boot path. It stays off this registry's runtime surface because + * the save door already runs the same compile itself: `findHtmlPageSourceGaps` + * in `@objectstack/metadata-protocol`'s `runtime-authoring-gate.ts` imports the + * same `compile()` and runs it against the deployment's SDUI component manifest, + * reports under the same `jsx-CODE` rule ids, and adds the page's `requires` + * check (`page-requires-disagrees-with-source`). Wiring this entry there too + * would judge every html page twice. + * + * The two differ in one case: with no manifest this rule still checks syntax + * and structure, while a host that registered no manifest has its save door + * judge nothing and says so once at boot. + */ +const RUNTIME_HTML_SOURCE_COMPILED_AT_SAVE = + 'Runtime-safe (the dependency-free @objectstack/sdui-parser, which never executes the source) but ' + + 'not wired here: the save door already compiles an html page\'s source itself, with the same ' + + 'compiler against the deployment\'s SDUI component manifest and under the same jsx-* rule ids ' + + '(metadata-protocol\'s findHtmlPageSourceGaps), so a second run would judge each page twice.'; + /** * The rule judges an OBJECT/field declaration at `advisory` tier — it can * never refuse a write (`tier: 'advisory'` means it never emits `error`, and @@ -1105,7 +1129,7 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [ commands: ALL, source: 'packages/lint/src/validate-jsx-pages.ts', surfaces: CLI_ONLY, - surfaceReason: RUNTIME_HEAVY_SOURCE_PARSE, + surfaceReason: RUNTIME_HTML_SOURCE_COMPILED_AT_SAVE, run: (stack, ctx) => validateJsxPages(stack, ctx.sduiManifest ? { manifest: ctx.sduiManifest as never } : {}), }, diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 26c6a70cd32..bdd629a8080 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -932,7 +932,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 + 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 | +| 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 | | 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 as a BODY key because the live one is the `sys_metadata` row column the door supplies, and `label` is container display metadata with no reader. Neither is `authorWarn`'d and both are deliberately KEPT — the platform's own writers send `name` (artifact-shipped containers, the metadata-validation sweep), so tombstoning it would reject shapes we write ourselves. `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) | | 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 | diff --git a/packages/spec/liveness/page.json b/packages/spec/liveness/page.json index b180f98f010..a866b58134a 100644 --- a/packages/spec/liveness/page.json +++ b/packages/spec/liveness/page.json @@ -7,8 +7,12 @@ "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." }, "requires": { - "status": "planned", - "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." + "status": "live", + "verifiedAt": "2026-10-02", + "evidenceScope": "in-repo", + "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", + "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", + "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." }, "name": { "status": "live", diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-html-page-div-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-html-page-div-refused.ts index 0fbab496b84..ecfa5b60cf3 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-html-page-div-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-html-page-div-refused.ts @@ -46,7 +46,15 @@ export const entry: SemanticMigration = { + 'own html-page compile refuses `div` the same way. A `div` in such a page, which used to ' + 'pass unchecked, now fails the command with `jsx-forbidden-tag` and ' + '`jsx-unknown-component`. A project that keeps its own `sdui.manifest.json` is checked ' - + 'against that file, as before.', + + 'against that file, as before. The runtime save door now holds pages to the same manifest: ' + + 'a server that `objectstack serve` runs (`dev` and `start` run it too) resolves the ' + + 'deployment\'s manifest the same way, from the `sdui.manifest.json` beside the served config ' + + 'and then the copy `@objectstack/console` ships, and the metadata save door compiles an html ' + + 'page\'s source against it on every publish. A `div` page saved from Studio or through the ' + + 'metadata API is refused with a `422` under the same rule ids, and a draft is stored as ' + + 'written and refused at its publish. A server that resolves no manifest says so once at boot ' + + 'and stores html pages unjudged, as before. Pages already stored are not rewritten; each is ' + + 'judged the next time it is saved.', acceptanceCriteria: '`objectstack validate` reports no `jsx-forbidden-tag`, `jsx-unknown-component` or ' + '`jsx-unknown-prop` finding on any `kind:\'html\'` page and prints no ' diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index 1498912490e..fb363a364c5 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -899,9 +899,18 @@ export const PageSchema = lazySchema(() => strictObject({ */ source: z.string().optional() .describe("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."), - /** Plugin namespaces the JSX source references — inferred at compile, checked at save AND load (ADR-0048 provenance). */ + /** + * Plugin namespaces an html page's source uses (ADR-0080 §5; ADR-0048 + * provenance). Derived from the source at save, so authors omit it. On a + * server that has the deployment's SDUI component manifest, the save door + * compiles a `kind: 'html'` page's source, refuses a written list that + * disagrees with it (a draft at its publish), and stores the derived one; + * at load, a stored page whose list names a plugin no manifest component + * carries is reported and still served. A server with no manifest judges + * neither, and says so at boot. + */ requires: z.array(z.string()).optional() - .describe('Plugin namespaces the JSX source references (validated at save and load)'), + .describe("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."), // ADR-0010 — runtime protection envelope (internal — set by the loader). // `page` is a registered metadata type, so `MetadataPlugin`'s loader stamps From 2fb84234e094cc07e57496e6001f33a464939688 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 14:38:11 +0000 Subject: [PATCH 2/3] chore(spec): regenerate the migration registry, page reference and liveness counts Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- content/docs/references/ui/page.mdx | 2 +- packages/spec/liveness/state-counts/page.md | 2 +- packages/spec/src/migrations/registry.ts | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index c761c535a75..1e0689019f1 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -184,7 +184,7 @@ View filter rule | **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). | | **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' (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. | -| **requires** | `string[]` | optional | Plugin namespaces the JSX source references (validated at save and load) | +| **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. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | | **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | diff --git a/packages/spec/liveness/state-counts/page.md b/packages/spec/liveness/state-counts/page.md index 25177d8eb2b..d2657358ae3 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` | 22 | 0 | 0 | 1 | 1 | 24 | +| `page` | 23 | 0 | 0 | 1 | 0 | 24 | diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 87acefd17c3..13eb4dbd80d 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -18721,7 +18721,15 @@ const step18: MigrationStep = { + 'own html-page compile refuses `div` the same way. A `div` in such a page, which used to ' + 'pass unchecked, now fails the command with `jsx-forbidden-tag` and ' + '`jsx-unknown-component`. A project that keeps its own `sdui.manifest.json` is checked ' - + 'against that file, as before.', + + 'against that file, as before. The runtime save door now holds pages to the same manifest: ' + + 'a server that `objectstack serve` runs (`dev` and `start` run it too) resolves the ' + + 'deployment\'s manifest the same way, from the `sdui.manifest.json` beside the served config ' + + 'and then the copy `@objectstack/console` ships, and the metadata save door compiles an html ' + + 'page\'s source against it on every publish. A `div` page saved from Studio or through the ' + + 'metadata API is refused with a `422` under the same rule ids, and a draft is stored as ' + + 'written and refused at its publish. A server that resolves no manifest says so once at boot ' + + 'and stores html pages unjudged, as before. Pages already stored are not rewritten; each is ' + + 'judged the next time it is saved.', acceptanceCriteria: '`objectstack validate` reports no `jsx-forbidden-tag`, `jsx-unknown-component` or ' + '`jsx-unknown-prop` finding on any `kind:\'html\'` page and prints no ' From 54c73b11ffd53a573eeed788529e8f54772237fa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 19:11:50 +0000 Subject: [PATCH 3/3] test(spec): re-ledger page requires as platform-written in the form reconciliation ledger The root omit row for page requires said "declared, not enforced yet", which the requires flip to live makes false. The ledger's own rule gives the offer decision to the enforcement, and the seat's answer of record (5959584348) is no form offer. The row now opens with the read-off family and the schema's own words, "derived from the source at save -- omit it", and states the measured truth per page kind: the save door stamps and judges an html/jsx page on a server with the deployment's SDUI manifest; on react, full and slotted pages nothing derives it and the load report is its one reader; the Studio page editor drops the key on every save. It moves to the platform-written group. No ruling is claimed and no contract changes. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- .../metadata-form-zod-reconciliation.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/spec/src/system/metadata-form-zod-reconciliation.test.ts b/packages/spec/src/system/metadata-form-zod-reconciliation.test.ts index 2350eaed10b..2694ee4f52c 100644 --- a/packages/spec/src/system/metadata-form-zod-reconciliation.test.ts +++ b/packages/spec/src/system/metadata-form-zod-reconciliation.test.ts @@ -323,6 +323,13 @@ const LEDGER: ReadonlyArray = [ key: 'system', why: "platform-written, never authored — the schema calls it the `Auto-injected system/audit field` marker, kept apart from `author-declared business fields`, and every writer is platform code (the injected-column provenance table, the search companion, audit-field governance). The record validator reads it on the write path and skips its required and multi-value checks for a flagged column, so a control would let an author claim a false provenance that silently switches those checks off", }, + { + kind: 'omit', + type: 'page', + path: ROOT_PATH, + key: 'requires', + why: "platform-written, never authored — the schema's own words: `derived from the source at save — omit it`. On an `html` / `jsx` page, on a server with the deployment's SDUI manifest, the save door stamps the compiled list and refuses a written list that disagrees (`page-requires-disagrees-with-source`); on `react`, `full` and `slotted` pages nothing derives it, and its one reader is the load report (a warning; the page is still served); and the Studio page editor drops the key on every save. A control would invite the list the describe tells every author to omit", + }, { kind: 'omit', type: 'view', @@ -398,13 +405,6 @@ const LEDGER: ReadonlyArray = [ key: 'picklist', why: 'declared, not enforced yet — liveness verdict `planned` (the server-side resolution that serves a picklist-bound field its options is not landed). No offer until it is enforced; the field designer offering a picklist is a later Studio phase', }, - { - kind: 'omit', - type: 'page', - path: ROOT_PATH, - key: 'requires', - why: 'declared, not enforced yet — liveness verdict `planned` (ADR-0080: inferred at compile time; save/load enforcement of plugin presence is deferred). No offer until it is enforced; whether to offer it then is a ruling for the enforcement, not for this gate', - }, { kind: 'omit', type: 'action',