Repository navigation
Commit 586934e
Fixes #20101
Clause-②: no
## What this changes
`PageSchema` declares `type: PageTypeSchema.default('record')`
(`packages/spec/src/ui/page.zod.ts`), so a page authored without `type`
is a record page. `saveMetaItem` parsed the body with that default, then
stored the authored body verbatim, and every `/meta` read serves a
stored row unparsed. A page saved without `type` was therefore stored
and served without it.
One helper, `withDeclaredPageTypeDefault` in
`packages/metadata-protocol/src/protocol.ts`, reads the default from the
registered `page` schema (`getMetadataTypeSchema('page').shape.type`,
the schema the save gate validates with). It fills the default only when
`type` is absent, so no `'record'` literal is added. It runs at two
seams:
- **Save.** It runs in `saveMetaItem` after the schema gate accepts the
body, beside the two existing grafts. New rows store the key, on draft
and publish saves alike.
- **Read.** It runs in `convertStoredItem`, the rehydration seam every
stored row passes through. Rows stored before this change are served
with the default. The row itself is never rewritten.
The read fill sits in `convertStoredItem` and not in
`convertStoredItemDetailed`. It is not an ADR-0087 conversion and emits
no notice, so `migrateStoredMetadata` (the Detailed caller) has nothing
to rewrite.
Scope is `PageSchema.type` only. The regions of `protocol.ts` that draft
PR #20125 edits are not touched: this diff's hunks are at the
`resolveOverlaySchema` docblock, after `graftFoldedFormSections`, in
`convertStoredItem` and in the `saveMetaItem` schema-gate block.
## Why both seams (H2, measured)
- **Read alone** covers old rows, but it breaks #4326's invariant that a
GET → PUT round-trip is byte-identical. The served body would carry a
key the row does not have. The first re-save of every such page would
then write a one-key change, a new checksum and a history row that
nobody authored. Ablation leg B below shows this: with the save fill
removed, the round-trip pin goes red.
- **Save alone** leaves every row stored before this change typeless on
every read. Covering those rows would need a stored-row rewrite, which
triage point 2 forbids. Ablation leg A below shows this.
- **Both together** is the smallest fix that is correct. Measured below:
a new row round-trips with the same bytes, checksum and version, and an
old row is served typed without being rewritten.
## Serve paths: base `226e00c038` vs head (H1)
The measurement used a typeless page, an explicit `type: 'app'` control,
a typeless draft, and a typeless row seeded directly into the store (the
"old row"). It ran at three levels: the real `RestServer` over the real
protocol, the protocol alone, and the real composition
(`bootStack(showcase)` plus `MetadataPlugin` over a build-shaped
artifact, over real HTTP).
| Path | base: typeless | base: `app` control | head: typeless | head:
`app` control |
|:---|:---|:---|:---|:---|
| `saveMetaItem` publish: stored body | absent | `app` | `record` |
`app` |
| `saveMetaItem` draft: stored draft body | absent | n/a | `record` |
n/a |
| `publishMetaItem` of that draft: stored and served | absent | n/a |
`record` | n/a |
| Registry write-through on save (unscoped kernel) | absent | `app` |
`record` | `app` |
| `GET /meta/page` (`getMetaItems`), cache on and off, new and old row |
absent | `app` | `record` | `app` |
| `GET /meta/page?preview=draft` | absent | n/a | `record` | n/a |
| `GET /meta/page/:name`, cached arm (`getMetaItemCached`) | absent,
ETag `7811092d` | `app`, ETag `6164defa` | `record`, ETag `69c54eb6` |
`app`, ETag `6164defa` (unchanged) |
| `GET /meta/page/:name`, uncached arm (`getMetaItem`) | absent | `app`
| `record` | `app` |
| `GET /meta/page/:name?state=draft` | absent | n/a | `record` | n/a |
| `getMetaItem` with `previewDrafts` | absent | n/a | `record` | n/a |
| `getMetaItemLayered`, old row (overlay / effective) | absent / absent
| n/a | `record` / `record` | n/a |
| `searchAll` page hit `pageType` | absent | `app` | `record` | `app` |
| `loadMetaFromDb` boot hydration into the registry | absent | `app` |
`record` | `app` |
| Old row at rest, after all the reads above | absent | n/a | absent
(not rewritten) | n/a |
| `migrateStoredMetadata` preview | scanned 4, canonical 4, pending 0 |
| scanned 4, canonical 4, pending 0 | |
| Real composition over HTTP: list, single, `?state=draft`, published
draft | absent | `app` (ETag `100766d6`) | `record` | `app` (ETag
`100766d6`, unchanged) |
## Consumers of the served page body (H3)
Every measured reading below used the explicit `app` page as its
control.
- **Server-side record-page selection by `type`: none in this repo.** A
grep of non-test `packages/**` for reads of a page's `type` finds two
readers. One is the `searchAll` page sweep (`pageType`, measured). The
other is the lint rule in `validate-page-visualization-bindings.ts`,
which asks `page.type !== 'list'`; its verdict cannot change, because
`record` is not `list`. Record-page selection lives in objectui's
`usePageAssignment`, per the card, and that is UNMEASURED here because
objectui is not reachable from this session.
- **Measured to carry the fill:** the list, single, cached, draft-state,
preview and layered reads; boot hydration; the registry write-through;
and the `searchAll` sweep. See the table above.
- **Carry the fill by code reading, not separately exercised:** these
read through `convertStoredItem` or `getMetaItems`. They are the runtime
authoring gate's stored universe (`foldStoredCollection`), the
`duplicatePackage` copy (which is then saved through `saveMetaItem`),
and the runtime package-export sweep (`getMetaItems`).
- **Deliberately raw:** `historyMetaItem` and `diffMetaItem` history
bodies, which are a record of what was written, and `getEffectiveLock`,
which reads protection only.
- **Outside this card's surface, not filled:** the `metadata` service's
`DatabaseLoader.rowToData` (`packages/metadata`). In the composition
measured (showcase plus `MetadataPlugin`), every read of the test pages
served the typed body.
- **Hashes.** The row checksum (the `sha256:` token `If-Match` compares)
is computed over the stored body, so a read cannot move it. The cached
read's ETag is a hash of the served content. It changes once for a
typeless page and is unchanged for the `app` control.
- **Round-trip.** Serving the new row, then PUTting it back, stores the
same bytes, checksum and version: the repository's identical-body
short-circuit fires and nothing is written. Serving the old row and
PUTting it back stores `type` once, and the served body is identical
before and after. That is the author's re-save, not a migration.
## Tests
All runs are at head `e3b83df825`. The probes ran at `c2a29b4573`, and
`protocol.ts` is byte-identical between the two commits.
- `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2`: 188 files passed, 3 skipped; 2686 tests passed, 19
skipped; exit 0.
- `pnpm --filter @objectstack/metadata-protocol typecheck`: exit 0. `tsc
--listFiles` includes all 191 test files, among them the three edited
here.
- REST `/meta` route tests: every `packages/rest/src/*meta*.test.ts`, 47
files, 667 tests passed, exit 0. They read the rebuilt
`metadata-protocol` `dist/`, which contains
`withDeclaredPageTypeDefault` (4 hits).
- Dogfood metadata round-trip tests: `dashboard-designer-roundtrip`,
`meta-published-and-state-routes`, `package-first-authoring`,
`meta-types-create-seed`, `showcase-object-extension-meta-read` and
`showcase-object-extension-scalar-divergence`. 6 files, 33 tests passed,
exit 0.
New pins:
- `protocol.stored-conversions.test.ts`: a stored typeless page row is
served with the declared default by the list and single reads, the draft
read and the list preview, and boot hydration. An explicit `app` row is
untouched. The row's bytes survive the reads, and
`migrateStoredMetadata` preview and apply report it canonical, with
`rewritten: 0`.
- `protocol.read-decorations.test.ts`: publish and draft saves store the
default. An explicit `app` is stored and served unchanged. GET → PUT
gives the same stored body, checksum and version.
- `protocol.search-published-pages.test.ts`: a typeless page's hit
carries `pageType` equal to the declared default.
Every pin reads the expected value from the schema. A precondition
asserts that value is `record`.
**Ablation.** Each leg ran through `scripts/ablation-replace.mjs`
(anchor hit 1 → 0, blob changed) with a trap restore, and restore was
proven each time (blob `1df09c2eb4` equals HEAD, `git diff HEAD` empty).
These suites import `./protocol.js` from source, so no rebuild was
involved. `ablation-dist-preflight --absent` confirms the markers never
reached `dist/`.
- Leg A, read fill removed: 4 failed of 48. These are the three
stored-conversions read pins and the search pin.
- Leg B, save fill removed: 2 failed of 48. These are the stored-body
pin and the GET → PUT byte-identity pin.
- Restored: 48 of 48 passed.
## Gates
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, derived at `e3b83df825`: 61 commands. All
61 were run with exit codes recorded, and `--ran` reports 61 run, 0
NOT-MEASURED (a derived zero).
- `pnpm check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT
MET: 8 packages had no `dist/`). It passed after those were built.
- `node scripts/check-issue-citations.mjs --base 226e00c`: 5
citations, all resolve.
- Lint, narrowed: `eslint --no-inline-config --format json` over the 4
changed `.ts` files reports 4 files, 0 errors, 0 warnings.
`isPathIgnored` is false for them. `calculateConfigForFile` shows
neither `parserOptions.project` nor `projectService` set, so linting is
not type-aware and this diff cannot change the verdict on any file it
does not touch. The full `pnpm lint` is left to CI.
## Acceptance notes
- **Other declared defaults (triage point 3, census, not in this
diff).** Read from the built spec at `226e00c038`: 22 stored metadata
types were walked (types with `allowRuntimeCreate` or
`allowOrgOverride`). 15 of them declare 39 top-level defaults. The
positive control `page.type` is found, and the negative control
`page.object` (optional, no default) is not. `external_catalog` has no
registered schema. The save path stores all of these verbatim. Only
`page.type` was measured through the serve paths. The 38 besides
`page.type`:
- `object`: `isSystem`, `datasource`
- `hook`: `priority`, `async`, `onError`, `runAs`
- `seed`: `externalId`, `mode`, `env`
- `mapping`: `sourceFormat`, `mode`
- `view`: `type` (`simple`)
- `page`: `template`, `regions`, `isDefault`, `kind` (`full`)
- `app`: `active`, `isDefault`
- `action`: `type` (`script`), `refreshAfter`
- `report`: `type` (`tabular`), `drilldown`
- `flow`: `version`, `status`, `runAs`
- `datasource`: `active`, `autoConnect`, `schemaMode`, `origin`
- `email_template`: `category`, `locale`, `variables`, `active`,
`isSystem`
- `permission`: `isDefault`
- `position`: `delegatable`
- `skill`: `surface`, `active`
- The renderer half of triage point 4 ("is picked as the object's record
page") is objectui's `usePageAssignment`, and it is UNMEASURED from this
session. What this PR pins is the served body: `type` equal to the
declared default.
- The cached single read's ETag for a page stored without `type` changes
once, because the served content changed. The `app` control's ETag is
unchanged.
- The first re-save of an old typeless page stores `type` once, with one
history row. From then on its round-trip writes nothing.
- A stored row that spells the page type under a key `PageSchema`
refuses (for example the `pageType` alias key, which is reachable only
by a write that went around the save gate) is now read with `type:
'record'` beside that key. Such a row fails the schema either way and
carries `_diagnostics`. No such row was measured in any data.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8a44ce7 commit 586934e
5 files changed
Lines changed: 264 additions & 2 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 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
513 | 513 | | |
514 | 514 | | |
515 | 515 | | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| |||
133 | 134 | | |
134 | 135 | | |
135 | 136 | | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
136 | 153 | | |
137 | 154 | | |
138 | 155 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
255 | 256 | | |
256 | 257 | | |
257 | 258 | | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
693 | 693 | | |
694 | 694 | | |
695 | 695 | | |
696 | | - | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
697 | 699 | | |
698 | 700 | | |
699 | 701 | | |
| |||
1324 | 1326 | | |
1325 | 1327 | | |
1326 | 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 | + | |
1327 | 1377 | | |
1328 | 1378 | | |
1329 | 1379 | | |
| |||
4678 | 4728 | | |
4679 | 4729 | | |
4680 | 4730 | | |
| 4731 | + | |
| 4732 | + | |
| 4733 | + | |
| 4734 | + | |
| 4735 | + | |
| 4736 | + | |
| 4737 | + | |
4681 | 4738 | | |
4682 | 4739 | | |
4683 | 4740 | | |
4684 | | - | |
| 4741 | + | |
4685 | 4742 | | |
4686 | 4743 | | |
4687 | 4744 | | |
| |||
4692 | 4749 | | |
4693 | 4750 | | |
4694 | 4751 | | |
| 4752 | + | |
4695 | 4753 | | |
4696 | 4754 | | |
4697 | 4755 | | |
| |||
16120 | 16178 | | |
16121 | 16179 | | |
16122 | 16180 | | |
| 16181 | + | |
| 16182 | + | |
| 16183 | + | |
| 16184 | + | |
| 16185 | + | |
| 16186 | + | |
| 16187 | + | |
16123 | 16188 | | |
16124 | 16189 | | |
16125 | 16190 | | |
| |||
0 commit comments