Skip to content

Commit 462935d

Browse files
feat(spec): declare the publish-drafts request body, so the change note is part of the published API contract (#28447)
🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V Fixes #28406 Clause-②: yes (widening) Metadata-protocol: added ## 维护者速读 - **改了什么**:`POST /packages/:id/publish-drafts`(Studio「发布整个应用」)的请求体第一次写进了 `@objectstack/spec` 的公开契约:`PublishPackageDraftsRequestSchema` 声明 `message`(修改说明)和 `backfill`;路由台账这一行也写明了它。 - **为什么改**:#28422 让 Studio 发布时带上修改说明并写进每条历史记录,但这个字段只是服务端在读,契约里没有;SDK 和 API 用户看不到它。现在他们在类型和参考文档里都能看到。 - **风险与回滚**:线上行为一个字节都不变(服务端照旧读请求体);新契约不严格,旧 SDK 仍发送的 `actor` 被忽略、不会被拒。回滚即还原本 PR。 - **先看**:`packages/spec/src/api/protocol.zod.ts`(新 schema 与说明),`packages/runtime/src/domains/packages-publish-drafts-request-conformance.test.ts`(每个字段在服务端实测的行为),`packages/runtime/src/route-ledger.ts`(台账新增的 `requestSchema` 字段)。 - **两分钟验证**:`pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-publish-drafts-request-conformance.test.ts`,预期 8 个测试全过。 - **需要你做的**:无,席位落地。 ## What changes - **`@objectstack/spec/api`**: `PublishPackageDraftsRequestSchema` and its type `PublishPackageDraftsRequest`, beside `PublishPackageDraftsResponseSchema`. Two members, measured at the door (`packages/runtime/src/domains/packages.ts`, publish-drafts branch), not assumed: - `message`: optional string. The door forwards it only when it is a string. - `backfill`: optional array of `{ object, field }`, both non-empty strings, entries strict. No default: absent or `[]` asks for nothing. The door forwards it as sent; the protocol's shape judgement (`readPublishBackfillRequest`) refuses a malformed entry `400 VALIDATION_ERROR` before anything is published. The "named once" and "field is new and has a literal default" rules are described, not encoded: a refinement would be dropped from the published JSON Schema, and the second is judged against server state. - The object is **not strict**: unknown members are stripped, not refused. `actor` is deliberately not declared (the door records the authenticated caller), and `@objectstack/client`'s `packages.publishDrafts` still sends its deprecated `opts.actor`, so a strict object would describe that call as invalid. - **Route ledger** (`packages/runtime/src/route-ledger.ts`): the dispatcher ledger had no `requestSchema` field (only `responseSchema`). The field is added, runtime-internal like the rest of the ledger, with the same rules as `responseSchema` (a name, resolved against the spec exports, never filled without conformance coverage). Its JSDoc says what a filled row claims and what it does not. The publish-drafts row names `PublishPackageDraftsRequestSchema`. - **Resolver guard** (`packages/client/src/route-ledger-response-schema.test.ts`): ledgered `requestSchema` names resolve against the live spec exports, using the existing resolver, with an anti-vacuity pin. - **Regenerated by repo tooling**: `api-surface`, `export-origins`, `declaration-map`, `authorable-surface`, `json-schema.manifest`, `content/docs/references/**`, strictness-ledger counts. The SDK client is hand-written, not generated, and `packages/client/src/index.ts` is not touched. `gen:openapi` emits a fixed list of nine contract schemas and its output is gitignored, so nothing there moves. - Changeset: `@objectstack/spec` minor. ## Tests - `packages/spec/src/api/protocol.test.ts`: the schema accepts `{}`, `{ message }`, `{ backfill }` and both; strips the SDK's `actor`; refuses a non-string `message` and each malformed `backfill` shape with the zod issue code and path; declares exactly the two members. - `packages/runtime/src/domains/packages-publish-drafts-request-conformance.test.ts` drives the real handler: - each schema-accepted body reaches `publishPackageDrafts` exactly as the schema parses it; an absent body reads as `{}`; - a body `actor` gets 200 and never reaches the publish (the session user is recorded); - `aiModel`, `context` and `packageId` in the body are not read; - each malformed `backfill` is refused `400 VALIDATION_ERROR` naming the entry, through the real `ObjectStackProtocolImplementation` (its engine is a proxy that throws if touched, which shows the judgement runs before any read); - the ledger row names the schema. - `packages/spec/src/type-alias-convention.pin.test.ts`: the isomorphism pin for the new schema (805 → 806). ## Acceptance notes - **The door does not parse the body with the schema.** A non-string `message` is dropped and the publish goes ahead with no note: 200, not 400. The conformance suite pins this as a measured divergence, so a door that starts enforcing the schema flips one pin. Changing the door is outside this card's file surface (`packages/runtime/src/domains/packages.ts`, which #28414 is editing now). It is reported to the seat. - **One undeclared read remains on `main`:** the ADR-0045 app re-save after the publish still passes a body `actor` to `saveMetaItem`. #28414 removes that read. The conformance suite pins it today, and the pin's message says what to flip when #28414 lands. - #28414 had no open PR when this was opened, and its branch touches no `packages/spec` file, the route ledger or generated artefacts. ## Verification <details><summary>Local gates and tests (head sha in the os-dev-report comment on the card)</summary> Filled in the os-dev-report comment on #28406: `dispatch-gates --commands` families with exit codes, `--ran` reconciliation, and the reverse verification of the non-strict choice. </details> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 776c1d8 commit 462935d

15 files changed

Lines changed: 428 additions & 15 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): the body of `POST /packages/:id/publish-drafts` is declared, so the change note a publish carries is part of the published API contract
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: added
9+
10+
`@objectstack/spec/api` gains `PublishPackageDraftsRequestSchema` and its type `PublishPackageDraftsRequest`, beside the door's existing `PublishPackageDraftsResponseSchema`. It declares the two body members the door reads:
11+
12+
- **`message`** (optional string): the author's change note. The package commit records it, and so does every item history row the publish writes (`change_note`, read back as each history event's `message`). Absent or empty, those rows record the sentence naming the package.
13+
- **`backfill`** (optional array of `{ object, field }`, both non-empty strings, nothing else in an entry): fields the publish adds whose literal `defaultValue` is written into existing rows, as the signed-in caller. A malformed, repeated or unhonourable entry refuses the whole request `400 VALIDATION_ERROR` before anything is published.
14+
15+
The object is not strict: other members are stripped, not refused. `actor` is deliberately not declared, since the door records the authenticated caller; `@objectstack/client`'s `packages.publishDrafts` still sends its deprecated `opts.actor`, and that call keeps working unchanged. Nothing changes on the wire: the door reads the body as it did before this declaration.

‎content/docs/references/api/protocol.mdx‎

Lines changed: 22 additions & 3 deletions
Large diffs are not rendered by default.

‎content/docs/references/index.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Protocol reference — every schema by module
33
navTitle: Protocol Reference
4-
description: Every schema published by @objectstack/spec — 1573 schemas across 14 protocol modules
4+
description: Every schema published by @objectstack/spec — 1574 schemas across 14 protocol modules
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -21,7 +21,7 @@ counts are sums of the rows they head. Regenerate with
2121
| Module | Pages | Schemas | Description |
2222
| :--- | ---: | ---: | :--- |
2323
| [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. |
24-
| [API Protocol](/docs/references/api) | 34 | 452 | REST contracts, endpoints, routing, realtime, batch, discovery. |
24+
| [API Protocol](/docs/references/api) | 34 | 453 | REST contracts, endpoints, routing, realtime, batch, discovery. |
2525
| [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
2626
| [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
2727
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
@@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with
3434
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3535
| [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3636
| [UI Protocol](/docs/references/ui) | 16 | 174 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
37-
| **Total** | **204** | **1573** | 14 protocol modules |
37+
| **Total** | **204** | **1574** | 14 protocol modules |
3838

3939
---
4040

@@ -63,7 +63,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations.
6363

6464
## API Protocol
6565

66-
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 452 schemas**
66+
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 453 schemas**
6767

6868
REST contracts, endpoints, routing, realtime, batch, discovery.
6969

@@ -92,7 +92,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery.
9292
| [`package-api-assembled.zod.ts`](/docs/references/api/package-api-assembled) | `AssembledInstalledPackage`, `GetInstalledPackageResponse`, `InstalledPackageAtEitherStage`, `ListInstalledPackagesResponse` |
9393
| [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `ImportUnmanagedPackageRequest`, `ImportUnmanagedPackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` |
9494
| [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `ValidationMode` |
95-
| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `FlowRegistration`, `FlowRegistrationRefusal`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` |
95+
| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `FlowRegistration`, `FlowRegistrationRefusal`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsRequest`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` |
9696
| [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` |
9797
| [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` |
9898
| [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` |

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate.
1919

2020
| Dir | Sites |
2121
|---|---|
22-
| `api/` | 461 |
22+
| `api/` | 463 |

‎packages/client/src/route-ledger-response-schema.test.ts‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import { AUTH_ROUTE_LEDGER } from '../../plugins/plugin-auth/src/auth-route-ledg
5353
/** The five independent ledgers, labelled so a failure names the file to open. */
5454
const LEDGERS: ReadonlyArray<{
5555
readonly label: string;
56-
readonly rows: ReadonlyArray<{ route: string; responseSchema?: string }>;
56+
readonly rows: ReadonlyArray<{ route: string; responseSchema?: string; requestSchema?: string }>;
5757
}> = [
5858
{ label: 'packages/runtime/src/route-ledger.ts', rows: ROUTE_LEDGER },
5959
{ label: 'packages/rest/src/rest-route-ledger.ts', rows: REST_ROUTE_LEDGER },
@@ -185,3 +185,34 @@ describe('[#5791] every ledgered `responseSchema` names a real spec schema', ()
185185
).toContain('GET /api/v1/discovery');
186186
});
187187
});
188+
189+
/**
190+
* The request-side twin (ADR-0146 D1): `requestSchema`, so far on the dispatcher
191+
* ledger only, names the spec export declaring the BODY a route reads. Same
192+
* resolver, same reason — a name is data, and a typo or a retired schema must
193+
* fail here rather than rot. That a filled row has conformance coverage is
194+
* pinned beside the coverage itself (`packages-publish-drafts-request-conformance.test.ts`
195+
* in `packages/runtime`).
196+
*/
197+
describe('every ledgered `requestSchema` names a real spec schema', () => {
198+
const requestRows = () =>
199+
LEDGERS.flatMap(({ label, rows }) =>
200+
rows
201+
.filter((r): r is typeof r & { requestSchema: string } => r.requestSchema !== undefined)
202+
.map((r) => ({ ledger: label, route: r.route, requestSchema: r.requestSchema })),
203+
);
204+
205+
it('resolves each declared name against the live `@objectstack/spec/api` / `/api-assembled` exports', () => {
206+
const broken = requestRows()
207+
.map(({ ledger, route, requestSchema }) => {
208+
const why = resolutionFailure(requestSchema);
209+
return why ? `${ledger} — \`${route}\`: requestSchema '${requestSchema}' ${why}` : undefined;
210+
})
211+
.filter((m): m is string => m !== undefined);
212+
expect(broken, 'Ledger rows whose `requestSchema` does not resolve to an exported zod schema.').toEqual([]);
213+
});
214+
215+
it('anti-vacuity: the publish-drafts row declares its request body', () => {
216+
expect(requestRows().map((r) => r.route)).toContain('POST /packages/:id/publish-drafts');
217+
});
218+
});

0 commit comments

Comments
 (0)