Repository navigation
Commit c512c25
feat(spec, metadata-protocol): each _drafts row carries the draft body's own label, or null (#22323)
Fixes #22200
Clause-②: yes (widening: a new field on a published response schema)
## What changes
Each row of the pending-drafts list (`GET /api/v1/meta/_drafts`, the
runtime's `GET /metadata/_drafts`, `client.meta.listDrafts()`) now
carries `label`: the draft body's own top-level `label`, as authored, or
`null` when the body declares none. It is never the machine name
standing in for a missing label.
- **Contract** (`packages/spec/src/api/protocol.zod.ts`):
`ListDraftsResponseSchema` rows gain `label:
I18nLabelSchema.nullable()`, required on the wire, with a describe that
says it is the body's own label and never a name fallback.
- **Producer**
(`packages/metadata-protocol/src/sys-metadata-repository.ts`):
`SysMetadataRepository.listDrafts` reads the label off the
`sys_metadata` row it already fetches (`draftBodyLabel`), so there is no
second query. The declared return gains `label: I18nLabel | null`.
- **Pass-through** (`packages/metadata-protocol/src/protocol.ts`):
`ObjectStackProtocolImplementation.listDrafts` declares the same member
and passes the repository's rows through unchanged. The docblock on the
publish batch's closure read, which listed the six header members, now
lists seven.
The maintainer's direction on objectstack-ai/objectui#11862, quoted by
the card: 「所有地方以标签为主,机器名只作为次要信息」. The producer carries the label;
consumers do not each fetch one. Triage's grade, quoted: "Each `_drafts`
row carries the draft body's own label, or `null`"; "Add a row whose
draft body declares no label and reads `null`, never a fallback."
## Measurements behind the shape (each PM hypothesis, measured on this
branch's base `238222d8c`)
**H1: holds, with one refinement.** `listDrafts` calls
`engine.find('sys_metadata', { where, context })` with no field
projection, so the whole row is in hand. The body is NOT a column of its
own: `sys_metadata` has no `label` column
(`packages/metadata-core/src/objects/sys-metadata.object.ts`); the label
lives inside the `metadata` textarea column, stored as JSON text (an
already-parsed object on a JSON-column dialect). So the producer parses
`row.metadata` once per draft row, through the same `storedRowBody`
reader `rowToItem` uses. No second query.
Label spelling per metadata type, read from
`getMetadataTypeSchema(type)` over every
`DEFAULT_METADATA_TYPE_REGISTRY` entry (built `packages/spec/dist`,
`z.toJSONSchema`, input side):
| top-level `label` shape | types |
|:---|:---|
| plain `string` | object, field, hook, picklist, mapping, flow, job,
datasource, translation, email_template, doc, book, permission,
position, capability, agent, tool, skill |
| `I18nLabel` (string or inline locale map) | view (container and view
items), page, dashboard, app, action, report, dataset |
| no `label` key | seed, api (rows read `null`) |
| no schema | external_catalog (any stored `label` is judged by
`I18nLabelSchema`) |
No registered type spells its display label `title` or nests it.
Off-registry: `connector`, `sharing_rule` and `webhook` spell it `label`
(string); `analytics_cube` spells it `title` and its schema refuses
`label` with guidance to `title`, so a cube draft reads `null` (see
Acceptance notes).
No ADR-0087 conversion rewrites a top-level `label`
(`packages/spec/src/conversions/registry.ts`: the only `label` entries
are the nested `datasource.external.label` removal and fixtures), so the
stored spelling is already the canonical one and reading it raw needs no
conversion replay.
**H2: holds; no third file moves.** Both faces serve the protocol's
return whole: `packages/rest/src/rest-server.ts` `GET
${metaPath}/_drafts` ends in `res.json(result)`, and
`packages/runtime/src/domains/meta.ts` `_drafts` ends in
`deps.success(data)`. `packages/client/src/index.ts` `meta.listDrafts`
types its answer as the spec's `ListDraftsResponse`. `packages/rest` and
`packages/runtime` are untouched.
**H3: holds.** Seven types declare `label` as `I18nLabelSchema` (string
or inline locale map such as `{ en, 'zh-CN' }`). What the producer does
with each shape:
- **plain string**: carried verbatim.
- **inline locale map**: carried verbatim, not resolved. The route takes
no locale, so resolving here would be the producer choosing a language
for the reader; the reader resolves it the way it resolves every other
`I18nLabel` (`resolveI18nLabel` in `@objectstack/spec/ui`, or objectui's
`pickLocalized`).
- **absent / `null`**: `null`.
- **any other stored value** (a number, an array, the retired
key-reference form): `null`. `I18nLabelSchema.safeParse` is the judge,
so the declared field never carries a shape its own type rules out.
Draft saves are schema-validated (`resolveOverlaySchema` in
`saveMetaItem`), so this is reachable only for rows stored before a
type's schema was enforced on save, or for a type with no registered
schema.
- **stored bytes that do not parse**: `null`, and the draft stays
listed. This is `lockHead`'s answer for the same bytes, in the same
file, for the same reason: a header listing must not become a parse
failure. The draft stays visible and discardable, while every read of
its body still fails loudly.
## Landing and surface
The landing matches the claim's surface, plus one test file outside it,
named here with its reason:
- `packages/objectql/src/sys-metadata-repository-list-drafts.test.ts` is
a test of `SysMetadataRepository.listDrafts` that lives in
`packages/objectql`. Its #6599 disclosure pin asserted exactly six
header keys, so it goes red on any seventh. It now names seven keys. Its
fixture now carries the body in the `metadata` column the repository
actually reads, beside the two older spellings, so the whole-payload
sweep covers the real column. It asserts the label arrives while every
field-level secret stays off the wire. Its docblock said the routes had
"no capability gate"; both routes now gate on `mayReadPendingDrafts`, so
that sentence is corrected.
- Declared cross-lane files: `packages/metadata-protocol`
(`domain:engine`), declared by the seat on #6367.
## Tests
Readings at head `317b208be`. Every run went through
`scripts/pm/os-verify-lock.sh` on a shared box. The full report comment
on the card carries the rest.
- `@objectstack/spec` tests:
- project `local`, all 8 shards: 625 files, 18713 passed, 1 todo, 0
failed;
- project `repo`, shard 1/2: 27 files, 399 passed. The report states the
reading for shard 2/2, or NOT MEASURED with the reason;
- `typecheck` (tsc, scripts and the test layer) exits 0.
- `@objectstack/metadata-protocol` tests: the whole suite has 221 files
passed and 3 skipped, with 28287 tests passed and 19 skipped.
`typecheck` exits 0.
- `@objectstack/objectql`: `sys-metadata-repository-list-drafts.test.ts`
has 7 passed. `typecheck` (with the test layer) exits 0.
- `@objectstack/rest`: the 18 test files that name `_drafts` have 906
tests, all passed. `packages/rest` itself is unchanged.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands` was re-derived
after the change and gave 113 families. All 113 were run, every one
exited 0, and `--ran` reports "113 derived famil(ies) accounted for —
113 run, 0 NOT-MEASURED". `pnpm --filter @objectstack/spec
check:generated --fix` regenerated only
`content/docs/references/api/protocol.mdx`, and `check:docs` re-checked
green.
What the new pins cover:
- `@objectstack/spec`: the schema pin "ListDraftsResponseSchema declares
the pending-drafts body" now carries a string label, an
inline-locale-map label and a no-label row reading `null`. It also pins
that a row omitting `label` is refused at `drafts.0.label`.
- `@objectstack/metadata-protocol`
(`sys-metadata-repository-14938-list-drafts-updated-at.test.ts`, which
shares its pinned engine double): the declaration-keyed `CONFORMS` table
gains `label`, so every existing case also checks it. New `#22200` cases
cover:
- a label written through the real `put(..., { state: 'draft' })` and
read back;
- a body with no label, which reads `null`, not the name;
- a locale map carried verbatim;
- a JSON-column body;
- three off-spec labels, each reading `null`;
- torn stored bytes, which read `null` while the draft stays listed;
- `ObjectStackProtocolImplementation.listDrafts` passing the label
through, its response parsed by `ListDraftsResponseSchema` with every
member preserved, and exactly seven header keys with no body residue.
- Ablation, run once and not committed, through
`scripts/ablation-replace.mjs`: the producer line `label:
draftBodyLabel(row),` was replaced by the forbidden fallback `label:
row.name ?? null,`. All 10 `#22200` cases went red and the 8 `#14938`
cases stayed green. The file was restored to its HEAD blob and `git diff
HEAD` was empty.
- Reverse type verification: a `ListDraftsResponse` literal without
`label`, planted in a metadata-protocol test file, fails `tsc` with
`TS2741: Property 'label' is missing`. So the consumer reads the rebuilt
spec `.d.ts`. The file was restored to its HEAD blob.
## Acceptance notes (observations; not filed)
- **`analytics_cube` drafts read `label: null`.** `CubeSchema` spells
its display name `title` and refuses `label` with guidance to `title`.
This PR reads only `label`, the field the ruling names. A per-type
display-key mapping would be a second vocabulary for the producer to
keep in step, so it is not added here. Reach is not measured: no cube
draft producer was found in this repository. Carrier: none.
- **An empty-string `label` (`''`) is carried as `''`.** It is the
body's own value and passes `I18nLabelSchema`. Whether a reader treats
it as absent is the reader's choice.
- **objectui's half stays on objectstack-ai/objectui#11862.**
`MetadataDraftHeader` in `@object-ui/data-objectstack` follows after the
pin bump that carries this field. No objectui change is made here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ba8af87 commit c512c25
8 files changed
Lines changed: 311 additions & 17 deletions
File tree
- .changeset
- content/docs/references/api
- packages
- metadata-protocol/src
- objectql/src
- spec/src/api
| 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 | |
|---|---|---|---|
| |||
2087 | 2087 | | |
2088 | 2088 | | |
2089 | 2089 | | |
2090 | | - | |
| 2090 | + | |
2091 | 2091 | | |
2092 | 2092 | | |
2093 | 2093 | | |
2094 | 2094 | | |
2095 | 2095 | | |
2096 | 2096 | | |
2097 | 2097 | | |
| 2098 | + | |
2098 | 2099 | | |
2099 | 2100 | | |
2100 | 2101 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22833 | 22833 | | |
22834 | 22834 | | |
22835 | 22835 | | |
22836 | | - | |
| 22836 | + | |
| 22837 | + | |
| 22838 | + | |
| 22839 | + | |
22837 | 22840 | | |
22838 | 22841 | | |
22839 | 22842 | | |
| |||
22843 | 22846 | | |
22844 | 22847 | | |
22845 | 22848 | | |
| 22849 | + | |
| 22850 | + | |
22846 | 22851 | | |
22847 | 22852 | | |
22848 | 22853 | | |
| |||
22909 | 22914 | | |
22910 | 22915 | | |
22911 | 22916 | | |
22912 | | - | |
22913 | | - | |
| 22917 | + | |
| 22918 | + | |
| 22919 | + | |
22914 | 22920 | | |
22915 | 22921 | | |
22916 | 22922 | | |
| |||
Lines changed: 167 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
| 69 | + | |
| 70 | + | |
69 | 71 | | |
| 72 | + | |
70 | 73 | | |
71 | 74 | | |
72 | 75 | | |
| |||
107 | 110 | | |
108 | 111 | | |
109 | 112 | | |
| 113 | + | |
| 114 | + | |
110 | 115 | | |
111 | 116 | | |
112 | 117 | | |
| |||
346 | 351 | | |
347 | 352 | | |
348 | 353 | | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
Lines changed: 44 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| |||
195 | 196 | | |
196 | 197 | | |
197 | 198 | | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
198 | 234 | | |
199 | 235 | | |
200 | 236 | | |
| |||
1331 | 1367 | | |
1332 | 1368 | | |
1333 | 1369 | | |
| 1370 | + | |
| 1371 | + | |
| 1372 | + | |
| 1373 | + | |
| 1374 | + | |
| 1375 | + | |
1334 | 1376 | | |
1335 | 1377 | | |
1336 | 1378 | | |
| |||
1350 | 1392 | | |
1351 | 1393 | | |
1352 | 1394 | | |
| 1395 | + | |
| 1396 | + | |
1353 | 1397 | | |
1354 | 1398 | | |
1355 | 1399 | | |
| |||
0 commit comments