Skip to content

Commit 1818998

Browse files
Jack Qclaude
andauthored
feat(spec,objectql,metadata-protocol): validate-only data operation — DataProtocol.validateData (#6474)
* feat(spec,objectql,metadata-protocol): validate-only data operation (#6037) #4633 ruling D: import's dry run stops PREDICTING the write's verdict with a hand-copied mirror of the engine's rules and starts ASKING for it. `DataProtocol.validateData` reports the write path's verdict on candidate rows and persists nothing. Declaration and execution land together — a ruling clause, not a style note: `BatchOptions.validateOnly` was retired in #4052 as a dry-run flag that promised a preview while the batch surfaces persisted regardless. The new operation avoids that spelling and leaves the tombstone standing. `engine.validate()` calls the same validateRecord / evaluateValidationRules that insert() calls, so preview == write is guaranteed by construction; a test asserts it by running both against one engine under both ADR-0104 postures. The response carries the posture it was reached under — a bad value shape is an error on a self-certified deployment and an admitted warning on a warn-first one, which is why option B (unconditional strict) was rejected. Two documented boundaries: no hooks run (firing user hooks in a preview would be the #4052 defect respelled), and warn-first admissions are not recorded as #4769 certification evidence (a preview writes nothing, so it must not block a later migration). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3U9PJm1hEJitS9LtZz8TC * spec: regenerate strictness-ledger counts after merging main (#6037) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3U9PJm1hEJitS9LtZz8TC * test(spec): pin ValidateData* aliases as isomorphic (ADR-0122) The three new protocol aliases carry no defaults/transforms, so z.input and z.infer coincide — per ADR-0122 the complement is pinned in the registry rather than given a permanent-synonym XParsed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY * test(spec): pin-count receipt 751 -> 754 for the ValidateData* pins The registry's own count case documents every movement; record the three-pin rise with its cause per the file's idiom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 474f131 commit 1818998

15 files changed

Lines changed: 824 additions & 12 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/objectql": minor
4+
"@objectstack/metadata-protocol": minor
5+
---
6+
7+
feat(spec,objectql,metadata-protocol): validate-only data operation — ask for the write's verdict instead of predicting it (#6037, #4633 ruling D)
8+
9+
`import`'s dry run predicted the write path's verdict with a hand-copied mirror
10+
of the engine's rules (`rest/src/import-coerce.ts`). A copy cannot structurally
11+
keep up with the family it mirrors — ADR-0104 value shapes, `format` checks,
12+
object-level `validations`, the state machine — so ruling D replaces prediction
13+
with the verdict itself.
14+
15+
**New:** `DataProtocol.validateData(request)` returns the write path's verdict
16+
for candidate rows and persists nothing.
17+
18+
```ts
19+
const verdict = await protocol.validateData({
20+
object: 'lead',
21+
mode: 'insert', // or 'update', which judges only supplied keys
22+
data: [{ first_name: 'John', email: 'not-an-email' }],
23+
});
24+
// → { valid: false,
25+
// results: [{ valid: false, errors: [{ field: 'email', code: 'invalid_email', … }], warnings: [] }],
26+
// posture: { valueShapeStrict: true, mediaValueShapeStrict: false } }
27+
```
28+
29+
**Declaration and execution land together, deliberately.** `engine.validate()`
30+
(objectql) calls the same `validateRecord` / `evaluateValidationRules` that
31+
`insert()` calls, and `metadata-protocol` implements `validateData` on top of
32+
it. Agreement between preview and write is therefore guaranteed by
33+
construction, and a test asserts it directly by running both against one engine
34+
in both postures. This is the ruling's own clause, not a style choice:
35+
`BatchOptions.validateOnly` was retired in #4052 as a flag that promised a dry
36+
run while the batch surfaces persisted regardless, so a caller previewing a
37+
mutation had it EXECUTED. The new operation avoids that spelling too — the
38+
tombstone still stands and still rejects `validateOnly`.
39+
40+
**The verdict is the target deployment's, not an absolute.** The response
41+
carries the ADR-0104 `posture` it was reached under. On a self-certified
42+
deployment a bad value shape is an error; on a warn-first one the same row is
43+
valid and the finding appears in `warnings` with the same `code` — one finding
44+
that changed buckets, not two vocabularies. An unconditionally-strict preview
45+
was considered and rejected (#4633 option B): it would fail rows on every
46+
un-migrated deployment that the write would have accepted, which teaches
47+
authors to distrust the one gate in front of a bulk import.
48+
49+
Two boundaries worth knowing, both deliberate and both documented at the
50+
implementation:
51+
52+
- **No hooks run.** `beforeInsert` fires before validation on the real path, so
53+
a hook deriving a *business* field could change a verdict this does not
54+
simulate. Firing arbitrary user hooks in a preview — mail, outbound calls,
55+
writes to other objects — is the #4052 defect in a new spelling, so the gap is
56+
documented rather than closed. Audit/ownership stamps are `system`/`readonly`
57+
and validation skips them regardless.
58+
- **Warn-first admissions are not recorded as certification evidence.** The
59+
`#4769` sink exists so a boot cannot certify a contract it has just written
60+
against; a preview writes nothing, so recording there would let a *preview*
61+
block a later migration.
62+
63+
Additive: `validateData` is optional on `DataProtocol`, and nothing existing
64+
changes shape. `valueShapeStrictEffective` / `mediaStrictEffective` are now
65+
exported from objectql's record validator so the response reports the posture
66+
that actually decided the verdict rather than the raw deployment flag.
67+
68+
Unblocks #4633's consumption half (rest/import adopting the operation and
69+
retiring the `import-coerce.ts` mirror).

content/docs/references/api/protocol.mdx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ description: Protocol protocol schemas
1212
## TypeScript Usage
1313

1414
```typescript
15-
import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, CreateViewRequestSchema, CreateViewResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DeleteViewRequestSchema, DeleteViewResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, GetViewRequestSchema, GetViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, ListViewsRequestSchema, ListViewsResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, UpdateViewRequestSchema, UpdateViewResponseSchema } from '@objectstack/spec/api';
16-
import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse } from '@objectstack/spec/api';
15+
import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, CreateViewRequestSchema, CreateViewResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DeleteViewRequestSchema, DeleteViewResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, GetViewRequestSchema, GetViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, ListViewsRequestSchema, ListViewsResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, UpdateViewRequestSchema, UpdateViewResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api';
16+
import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api';
1717

1818
// Validate data
1919
const result = AiAgentCapabilitiesSchema.parse(data);
@@ -1614,3 +1614,44 @@ Uninstall package response
16141614

16151615
---
16161616

1617+
## ValidateDataIssue
1618+
1619+
### Properties
1620+
1621+
| Property | Type | Required | Description |
1622+
| :--- | :--- | :--- | :--- |
1623+
| **field** | `string` || The field the finding is about (`_record` for an object-level rule). |
1624+
| **code** | `string` || Machine-readable finding code, e.g. `required`, `invalid_type`, `rule_violation`. |
1625+
| **message** | `string` || Human-readable message — a validation rule's author-written text where one exists. |
1626+
1627+
1628+
---
1629+
1630+
## ValidateDataRequest
1631+
1632+
### Properties
1633+
1634+
| Property | Type | Required | Description |
1635+
| :--- | :--- | :--- | :--- |
1636+
| **object** | `string` || The object name. |
1637+
| **data** | `Record<string, any> \| Record<string, any>[]` || A candidate record, or an array of them. Nothing is persisted. |
1638+
| **mode** | `Enum<'insert' \| 'update'>` | optional | Which write the verdict should predict. `insert` (default) walks every declared field, so a missing required field is a finding; `update` judges only the supplied keys, matching a PATCH. |
1639+
1640+
1641+
---
1642+
1643+
## ValidateDataResponse
1644+
1645+
### Properties
1646+
1647+
| Property | Type | Required | Description |
1648+
| :--- | :--- | :--- | :--- |
1649+
| **object** | `string` || The object name. |
1650+
| **mode** | `Enum<'insert' \| 'update'>` || The write mode the verdict was reached for. |
1651+
| **valid** | `boolean` || True when EVERY row is valid — the whole-set answer. |
1652+
| **results** | `{ valid: boolean; errors: { field: string; code: string; message: string }[]; warnings: { field: string; code: string; message: string }[] }[]` || Per-row verdicts, in submission order. |
1653+
| **posture** | `{ valueShapeStrict: boolean; mediaValueShapeStrict: boolean }` || The ADR-0104 posture the verdict was reached under — reported because it is the difference between "this row is fine" and "this row is fine HERE". The same row can be an error on a self-certified deployment and an admitted warning on an un-migrated one, and a caller explaining a verdict needs to know which it got. An unconditionally-strict preview was considered and rejected (#4633 option B): it would fail rows on every un-migrated deployment that the write would have accepted. |
1654+
1655+
1656+
---
1657+

0 commit comments

Comments
 (0)