Commit cfa4d74
fix(spec,lint): page
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>requires is live — refused at save, reported at load (#21451)1 parent 99589f9 commit cfa4d74
10 files changed
Lines changed: 85 additions & 18 deletions
File tree
- .changeset
- content/docs/references/ui
- packages
- lint/src
- spec
- liveness
- state-counts
- src
- migrations
- entries/semantic
- system
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
184 | 184 | | |
185 | 185 | | |
186 | 186 | | |
187 | | - | |
| 187 | + | |
188 | 188 | | |
189 | 189 | | |
190 | 190 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
441 | 441 | | |
442 | 442 | | |
443 | 443 | | |
444 | | - | |
| 444 | + | |
445 | 445 | | |
446 | 446 | | |
447 | 447 | | |
448 | 448 | | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
449 | 452 | | |
450 | 453 | | |
451 | 454 | | |
452 | 455 | | |
453 | 456 | | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
454 | 478 | | |
455 | 479 | | |
456 | 480 | | |
| |||
1111 | 1135 | | |
1112 | 1136 | | |
1113 | 1137 | | |
1114 | | - | |
| 1138 | + | |
1115 | 1139 | | |
1116 | 1140 | | |
1117 | 1141 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
966 | 966 | | |
967 | 967 | | |
968 | 968 | | |
969 | | - | |
| 969 | + | |
970 | 970 | | |
971 | 971 | | |
972 | 972 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
11 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
12 | 16 | | |
13 | 17 | | |
14 | 18 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
0 commit comments