Repository navigation
Commit 250dec8
feat(metadata-protocol): a stored page naming an absent plugin is reported at load, and draft promotion re-stamps requires (#21121)
Fixes #20870
Clause-②: no
## Summary
Stage ③, engine half, of #20312 (ruling `5881821895`, letter A, staged
①②③; ruling `5902378057`, A + E). Stages ① and ② landed as #20852
(`b531c7bf0`). The spec half (liveness row, describe, docs) is #20871
and is not in this PR.
- **At load, report.** `loadMetaFromDb`, the boot hydration of stored
`sys_metadata` rows, now prints one `warn` line for each stored page
whose `requires` names a namespace that no component in the deployment's
SDUI manifest carries. The line names the page and every such namespace,
under the marker `[page_requires_plugin_absent]`. The report runs only
after the page has loaded, and it changes nothing about the page: the
page is registered and served. The manifest is read through
`resolveSduiManifest()`, the same `SDUI_MANIFEST_SERVICE` read the save
door makes. The namespace set comes from one helper,
`manifestNamespaces()` in `runtime-authoring-gate.ts`, which the save
door's `requires` judgement now uses too. There is no second manifest
reader.
- **Draft to active promotion re-stamps `requires`.**
`promoteDraftForPublish` serves both `publishMetaItem` (`POST
/meta/:type/:name/publish`) and `publishPackageDrafts`. It now passes
`SysMetadataRepository.promoteDraft` a new optional `deriveActiveBody`.
That callback applies the save door's own `stampHtmlPageRequires` to the
draft row being promoted, against the manifest read at publish time. The
active row therefore stores what an active save of the same body would
store.
- **Measure first (the Studio):** see the readings below. The Studio
does not compute `requires` on the client. It does send back the stored
`requires` it last read, and that sent-back stamp is what goes stale.
Example line (from the built `dist`, on this branch):
```text
[Protocol] [page_requires_plugin_absent] stored page/landing requires 'plugin-kanban', a namespace no component in this deployment's SDUI component manifest carries: the plugin that provides it is not loaded in the console this deployment serves. The page is loaded and served anyway, and what its source draws from that plugin will not render. Install it in that console, or take its components out of the page's source and save the page without `requires` — it is derived from the source at save.
```
## Measurements (taken before the code)
**H1 — the load path and its ordering.**
- A stored page reaches the runtime in one place: `loadMetaFromDb`
(env-wide `active` rows), called from `ObjectQLPlugin.start()` →
`restoreMetadataFromDb`.
- `page` is `allowOrgOverride: false`
(`packages/spec/src/kernel/metadata-plugin.zod.ts:856`), so no
org-scoped page row is minted. Legacy org-scoped residue is already
reported by `reportUnhydratableOrgScopedRows`.
- Package install (`installPackage`) records the package's own manifest
in the registry and writes no page row. Code-package pages are not
stored rows, and `os build` stamps no `requires` on them.
- `os serve` registers the manifest before the first `kernel.use(...)`
(`packages/cli/src/commands/serve.ts`, the block above "Load plugins
from configuration"). `os dev` compiles and spawns `serve`. Hydration
runs in a `start()`, after every `init()`. So the manifest is readable
when pages load.
- **Hook chosen: the hydration itself (`loadMetaFromDb`), not
`kernel:ready`.** It is literally the moment stored pages load. It
inherits hydration's own topology gating, so a project kernel that skips
hydration also skips the report, and it needs no second `sys_metadata`
read. The conclusion it records is drawn from a registered manifest's
contents, never from "nothing is registered". So the
`check:startup-registry-verdict` shape does not apply. It passed in the
gate run below.
- **Residual:** a host that registers the manifest after `start()` gets
no load report for that boot. No in-repo host does this. cloud#2482
should register it the way `os serve` does.
**H2 — no manifest.** The save door judges nothing without a manifest,
and the host says so at boot. The load report takes the same posture:
`findPageRequiresAbsentFromManifest` answers `null` and nothing is
printed. The `[page_requires_plugin_absent]` line can appear only when a
usable manifest is registered. An unusable registered value gets the
protocol's existing single warning and is not read.
**H3 — what promotion did before this PR.** The new pins were run
against unmodified `origin/main` `fed0db8f6`. Result: 4 red, 3 green
(the 3 green are controls).
- A draft saved before a manifest existed was promoted with **no**
`requires` (`expected undefined to deeply equal [ 'ui', 'plugin-kanban'
]`), on both `publishMetaItem` and `publishPackageDrafts`.
- An agreeing draft list was **carried forward as spelled** (`[
'plugin-kanban', 'ui', 'ui' ]`).
- A **disagreeing** list was already refused: the gate runs on the draft
body before `promoteDraft` (existing pin "a draft is not gated but its
publish is").
So "re-stamps" here means the promoted body carries the recomputed list
where it used to carry the draft's. Nothing new is refused, which is why
the line stays `Clause-②: no`.
**H4 — does the Studio send a client-computed `requires`?** Read from
objectui at the console pin `e420df310f5b` (`.objectui-sha`):
- **No client computation.** The only client `compile` call on a page is
the render path,
`packages/components/src/renderers/layout/page.tsx:606`. It destructures
`tree` and `diagnostics` and drops `requires`. No non-test source in
`packages/app-shell`, `packages/components`, `packages/react` or
`apps/console` assigns a page's `requires`; a word-bounded grep finds
only an action-preview i18n key.
- **But the stored stamp is sent back.** `SourcePageEditor.tsx:149`
patches only `{ source }` into the draft.
`ResourceEditPage.tsx:1033-1038` seeds that draft from the served
document. `ResourceEditPage.tsx:1443` and `:1477-1481` send the whole
draft back with `mode: 'draft'`. No per-type `fromDraft` exists for
`page`.
- **What that costs, measured on this branch** (a one-off probe, file
restored to its HEAD blob afterwards): save a page whose components are
all in the `ui` namespace (stamped `['ui']`) and publish it. Then add a
kanban component to its source and send the whole document back. The
draft keeps `['ui']`, and the publish answers `422 INVALID_METADATA`,
`page-requires-disagrees-with-source: 'plugin-kanban' is used by the
source but not listed`. The author never wrote `requires`. This PR does
not change that refusal (stage ②'s accept set); it is reported to the PM
as a finding and an open question.
## Pins
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`,
block `stored html page requires at load and at draft promotion
(#20312)`, 7 cases:
- at load, a stored page naming a plugin the manifest does not carry is
reported (page and plugin named, the present `ui` not named) and still
loads (`loaded: 1`, registered);
- at load, all plugins present: no report (the control);
- at load, no manifest: no report (H2);
- a draft saved before the manifest arrived is promoted with exactly
`stampHtmlPageRequires(...)`'s `requires`;
- an agreeing draft list is promoted as the save door spells it;
- the `publishPackageDrafts` batch re-stamps too;
- a promotion with no manifest stores the draft as written.
**Ablations** (one-shot, through `scripts/ablation-replace.mjs`; each
restore verified as blob equal to HEAD with `git diff HEAD` empty):
- The load-report call disabled (`... && false`): 1 of 7 red, the
reporting case.
- `deriveActiveBody` ignored (`const activeBody = draft.body;`): 3 of 7
red, the single, agreeing and batch promotion cases.
## Verification
At `c7b8b6399` (the head this PR opens on, after merging `origin/main`):
- `@objectstack/metadata-protocol` full suite: 196 files passed / 3
skipped, 2938 tests passed / 19 skipped; `typecheck` exit 0
(`--listFiles` includes the gate test file).
- Downstream consumers of `@objectstack/metadata-protocol` (the
`...@objectstack/metadata-protocol` direction), the files that drive
`promoteDraft` and boot hydration, at the first merge `7402cf72a`:
objectql (`--project local`) 6 files / 174 tests, rest 2 / 27, runtime 1
/ 5, all green.
- `node scripts/pm/dispatch-gates.mjs --commands` (no paths, from the
merge base): 62 families. All 62 exit 0 at `c7b8b6399`. The three that
read every package's `dist/` (`check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:type-check-debt`) ran after a full
`turbo run build` of `./packages/*` on that head; before that build they
answered `PREREQUISITE NOT MET` (exit 3), which is recorded here as a
non-measurement, not a pass. `--ran` reconciliation: 62 run, 0 NOT
MEASURED, every line recorded with its exit code.
- Driver conformance ledger: 50 / 0 / 0 before and after.
- Lint, narrowed and declared. The population is eslint's own:
`eslint.config.mjs` lints `packages/**` TS outside `packages/spec`.
`--format json` counted 4 files (the four touched sources): 0 errors, 0
warnings. The config never enables type-aware linting
(`eslint.config.mjs` notes no `parserOptions.project` and no typed
rules), so this diff cannot move any untouched file's verdict. The
repo-wide `pnpm lint` is CI's.
## Declared deviation from the claimed file surface
The claim names the pins beside
`protocol.runtime-authoring-gate.test.ts`, `protocol.ts`'s publish path,
`runtime-authoring-gate.ts` and the changeset. This PR also edits
`packages/metadata-protocol/src/sys-metadata-repository.ts`. The
draft-to-active write happens inside
`SysMetadataRepository.promoteDraft`, which reads the draft row itself.
The new optional `deriveActiveBody` applies the stamp to that same row,
so the derivation and the write read one row, not two. The option is
additive: omitted, the draft body is promoted byte for byte. It is named
in the changeset, which bumps `@objectstack/metadata-protocol` `minor`.
No other lane is touched: the load path is in the metadata layer, not
`packages/runtime` or `packages/cli`.
## Acceptance notes
- **The Studio's sent-back `requires` (H4).** This is the reason a stale
stamp exists at all in Studio editing. Proposed fix: objectui's page
editor omits `requires` from the body it saves, as the save door's own
hint says ("omit it"). Handed to the PM as a finding with the evidence
above. Not fixed here.
- The host's no-manifest boot line
(`packages/cli/src/utils/sdui-manifest.ts:270`) still says "not
validated at save". It is still true. With this PR the load report is
skipped on that host as well, so the line could say "at save or load".
Not edited here: cross-lane, pinned in the CLI's tests, and not false.
- On the `package-author` authoring channel (the control-plane assembly;
gate off), a disagreeing draft `requires` is promoted as written. That
matches what the save door stores on that channel.
- `POST /packages/:id/publish` (`MetadataManager.publishPackage`) is the
loader-plane snapshot publish over the in-memory registry. It is not a
`sys_metadata` draft promotion and never ran the save door, so it is
untouched.
- The load report is printed once per boot per affected page. It is not
deduplicated across boots, so each boot restates it until the page or
the console changes.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 840ec9d commit 250dec8
5 files changed
Lines changed: 332 additions & 9 deletions
File tree
- .changeset
- packages/metadata-protocol/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| |||
1232 | 1233 | | |
1233 | 1234 | | |
1234 | 1235 | | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
| 1239 | + | |
| 1240 | + | |
| 1241 | + | |
| 1242 | + | |
| 1243 | + | |
| 1244 | + | |
| 1245 | + | |
| 1246 | + | |
| 1247 | + | |
| 1248 | + | |
| 1249 | + | |
| 1250 | + | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
| 1277 | + | |
| 1278 | + | |
| 1279 | + | |
| 1280 | + | |
| 1281 | + | |
| 1282 | + | |
| 1283 | + | |
| 1284 | + | |
| 1285 | + | |
| 1286 | + | |
| 1287 | + | |
| 1288 | + | |
| 1289 | + | |
| 1290 | + | |
| 1291 | + | |
| 1292 | + | |
| 1293 | + | |
| 1294 | + | |
| 1295 | + | |
| 1296 | + | |
| 1297 | + | |
| 1298 | + | |
| 1299 | + | |
| 1300 | + | |
| 1301 | + | |
| 1302 | + | |
| 1303 | + | |
| 1304 | + | |
| 1305 | + | |
| 1306 | + | |
| 1307 | + | |
| 1308 | + | |
| 1309 | + | |
| 1310 | + | |
| 1311 | + | |
| 1312 | + | |
| 1313 | + | |
| 1314 | + | |
| 1315 | + | |
| 1316 | + | |
| 1317 | + | |
| 1318 | + | |
| 1319 | + | |
| 1320 | + | |
| 1321 | + | |
| 1322 | + | |
| 1323 | + | |
| 1324 | + | |
| 1325 | + | |
| 1326 | + | |
| 1327 | + | |
| 1328 | + | |
| 1329 | + | |
| 1330 | + | |
| 1331 | + | |
| 1332 | + | |
| 1333 | + | |
| 1334 | + | |
| 1335 | + | |
| 1336 | + | |
| 1337 | + | |
| 1338 | + | |
| 1339 | + | |
| 1340 | + | |
| 1341 | + | |
| 1342 | + | |
| 1343 | + | |
| 1344 | + | |
| 1345 | + | |
| 1346 | + | |
| 1347 | + | |
| 1348 | + | |
| 1349 | + | |
| 1350 | + | |
| 1351 | + | |
| 1352 | + | |
| 1353 | + | |
| 1354 | + | |
| 1355 | + | |
| 1356 | + | |
| 1357 | + | |
| 1358 | + | |
| 1359 | + | |
| 1360 | + | |
| 1361 | + | |
| 1362 | + | |
| 1363 | + | |
| 1364 | + | |
| 1365 | + | |
| 1366 | + | |
| 1367 | + | |
| 1368 | + | |
| 1369 | + | |
| 1370 | + | |
| 1371 | + | |
| 1372 | + | |
| 1373 | + | |
| 1374 | + | |
| 1375 | + | |
| 1376 | + | |
| 1377 | + | |
| 1378 | + | |
| 1379 | + | |
| 1380 | + | |
| 1381 | + | |
| 1382 | + | |
| 1383 | + | |
| 1384 | + | |
| 1385 | + | |
| 1386 | + | |
| 1387 | + | |
| 1388 | + | |
| 1389 | + | |
| 1390 | + | |
| 1391 | + | |
| 1392 | + | |
| 1393 | + | |
| 1394 | + | |
| 1395 | + | |
| 1396 | + | |
| 1397 | + | |
| 1398 | + | |
0 commit comments